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 @@ +604-bullet-train \ 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 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + 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 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + 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 @@ + + + + + + + image/svg+xml + + + + + + + + + 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 + + + + + + + + + + + + 0 + 0 + 400 + 26 + + + + + File + + + + + + + + + + + + + + + + + + + Edit + + + + + + + + + + + + + + Help + + + + + + + View + + + + + + + + + + + + + + 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(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(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"; + // } + + // 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"; + // } + + // 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.arrival(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.arrivalgetLineSpeed(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 getCoupled(int row) const; + QSet getUncoupled(int row) const; + + bool isEdited() const; + bool commitChanges(); + bool revertChanges(); + + JobCategory getCategory() const; + db_id getJobId() const; + db_id getJobShiftId() const; + db_id getNewShiftId() const; + + int getStopRow(db_id stopId) const; + + const QSet &getRsToUpdate() const; + const QSet &getStationsToUpdate() const; + inline void markRsToUpdate(db_id rsId) { rsToUpdate.insert(rsId); } + + LineType getLineTypeAfterStop(db_id stopId) const; + + inline StopItem getItemAt(int row) const { return stops.at(row); } + + void uncoupleStillCoupledAtLastStop(); + void uncoupleStillCoupledAtStop(const StopItem &s); + + bool getStationPlatfCount(db_id stationId, int &platfCount, int &depotCount); + +#ifdef ENABLE_AUTO_TIME_RECALC + void rebaseTimesToSpeed(int firstIdx, QTime firstArr, QTime firstDep); +#endif + + + void setAutoUncoupleAtLast(bool value); + +signals: + void edited(bool val); + + void categoryChanged(int newCat); + void jobIdChanged(db_id jobId); + void jobShiftChanged(db_id shiftId); + void errorSetShiftWithoutStops(); //TODO: find better way to show errors + +public slots: + void setCategory(int value); + bool setNewJobId(db_id jobId); + void setNewShiftId(db_id shiftId); + +private slots: + void reloadSettings(); + + void onExternalShiftChange(db_id shiftId, db_id jobId); + void onShiftNameChanged(db_id shiftId); + + void onStationLineNameChanged(); + +private: + //To simulate acceleration/braking we add 4 km to distance + static constexpr double accelerationDistMeters = 4000.0; + + QVector stops; + + QSet rsToUpdate; + QSet stationsToUpdate; + + db_id mJobId; + db_id mNewJobId; + + db_id jobShiftId; + db_id newShiftId; + + JobCategory category; + JobCategory oldCategory; + + enum EditState + { + NotEditing = 0, + InfoEditing = 1, + StopsEditing = 2 + }; + + EditState editState; + + database& mDb; + + //TODO: do not store queries, prepare them when needed + query q_segPos; + query q_getRwNode; + query q_lineHasSt; + query q_getCoupled; + + command q_setArrival; + command q_setDeparture; + + command q_setSegPos; + command q_setSegLine; + command q_setStopSeg; + command q_setNextSeg; + command q_setStopSt; + command q_removeSeg; + command q_setPlatform; + + bool timeCalcEnabled; + bool autoInsertTransits; + bool autoMoveUncoupleToNewLast; + bool autoUncoupleAtLast; + +private: + void insertAddHere(int row, int type); + db_id createStop(db_id jobId, db_id segId, const QTime &time, int transit = 0); + db_id createSegment(db_id jobId, int num); + db_id createSegmentAfter(db_id jobId, db_id prevSeg); + void setStopSeg(StopItem &s, db_id segId); + void setNextSeg(StopItem &s, db_id nextSeg); + void destroySegment(db_id segId, db_id jobId); + void resetStopsLine(int idx, StopItem &s); + void propagateLineChange(int idx, StopItem &s, db_id lineId); + void deleteStop(db_id stopId); + int propageteTimeOffset(int row, const int msecOffset); + void insertTransitsBefore(const QPersistentModelIndex &stop); + void setStation_internal(StopItem &item, db_id stId, db_id nodeId); + void shiftStopsBy24hoursFrom(const QTime& startTime); + + friend class RSCouplingInterface; + bool startInfoEditing(); + bool startStopsEditing(); + bool endStopsEditing(); +}; + +#endif // STOPMODEL_H diff --git a/src/jobs/jobeditor/model/trainassetmodel.cpp b/src/jobs/jobeditor/model/trainassetmodel.cpp new file mode 100644 index 0000000..717d789 --- /dev/null +++ b/src/jobs/jobeditor/model/trainassetmodel.cpp @@ -0,0 +1,222 @@ +#include "trainassetmodel.h" + +#include + +#include +using namespace sqlite3pp; + +#include "rslistondemandmodelresultevent.h" +#include "utils/rs_utils.h" + +#include + +TrainAssetModel::TrainAssetModel(database& db, QObject *parent) : + RSListOnDemandModel(db, parent), + m_jobId(0), + m_mode(BeforeStop) +{ +} + +void TrainAssetModel::refreshData() +{ + if(!mDb.db()) + return; + + query q(mDb, "SELECT COUNT(1) FROM(" + "SELECT coupling.rsId,MAX(stops.arrival)" + " FROM stops" + " JOIN coupling ON coupling.stopId=stops.id" + " WHERE stops.jobId=? AND stops.arrival(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 TrainAssetModel::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 sub.rsId,sub.number,sub.name,sub.suffix,sub.type FROM(" + "SELECT coupling.rsId,rs_list.number,rs_models.name,rs_models.suffix,rs_models.type,MAX(stops.arrival)" + " FROM stops" + " JOIN coupling ON coupling.stopId=stops.id" + " JOIN rs_list ON rs_list.id=rsId" + " LEFT JOIN rs_models ON rs_models.id=rs_list.model_id" + " WHERE stops.jobId=?3 AND stops.arrival?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(3, m_jobId); + //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(m_mode == AfterStop) + q.bind(4, m_arrival.addSecs(60)); + else + q.bind(4, m_arrival); + 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); +} + +void TrainAssetModel::setStop(db_id jobId, QTime arrival, Mode mode) +{ + m_jobId = jobId; + m_arrival = arrival; + m_mode = mode; + + refreshData(); + clearCache(); +} diff --git a/src/jobs/jobeditor/model/trainassetmodel.h b/src/jobs/jobeditor/model/trainassetmodel.h new file mode 100644 index 0000000..1ca5234 --- /dev/null +++ b/src/jobs/jobeditor/model/trainassetmodel.h @@ -0,0 +1,34 @@ +#ifndef TRAINASSETMODEL_H +#define TRAINASSETMODEL_H + +#include "rslistondemandmodel.h" +#include + +class TrainAssetModel : public RSListOnDemandModel +{ + Q_OBJECT +public: + typedef enum { + BeforeStop, + AfterStop + } Mode; + + TrainAssetModel(sqlite3pp::database& db, QObject *parent = nullptr); + + // IPagedItemModel + // Cached rows management + virtual void refreshData() override; + + // TrainAssetModel + void setStop(db_id jobId, QTime arrival, Mode mode); + +private: + virtual void internalFetch(int first, int sortCol, int valRow, const QVariant &val) override; + +private: + db_id m_jobId; + QTime m_arrival; + Mode m_mode; +}; + +#endif // TRAINASSETMODEL_H diff --git a/src/jobs/jobeditor/rscoupledialog.cpp b/src/jobs/jobeditor/rscoupledialog.cpp new file mode 100644 index 0000000..be0fe0f --- /dev/null +++ b/src/jobs/jobeditor/rscoupledialog.cpp @@ -0,0 +1,276 @@ +#include "rscoupledialog.h" + +#include +#include +#include +#include +#include + +#include + +#include "model/rsproxymodel.h" + +#include "utils/rs_utils.h" + +#include + +RSCoupleDialog::RSCoupleDialog(RSCouplingInterface *mgr, RsOp o, QWidget *parent) : + QDialog (parent), + couplingMgr(mgr), + legend(nullptr), + op(o) +{ + engModel = new RSProxyModel(couplingMgr, op, RsType::Engine, this); + coachModel = new RSProxyModel(couplingMgr, op, RsType::Coach, this); + freightModel = new RSProxyModel(couplingMgr, op, RsType::FreightWagon, this); + + QGridLayout *lay = new QGridLayout(this); + + QFont f; + f.setBold(true); + f.setPointSize(10); + + QListView *engView = new QListView; + engView->setModel(engModel); + QLabel *engLabel = new QLabel(tr("Engines")); + engLabel->setAlignment(Qt::AlignCenter); + engLabel->setFont(f); + lay->addWidget(engLabel, 0, 0); + lay->addWidget(engView, 1, 0); + + QListView *coachView = new QListView; + coachView->setModel(coachModel); + QLabel *coachLabel = new QLabel(tr("Coaches")); + coachLabel->setAlignment(Qt::AlignCenter); + coachLabel->setFont(f); + lay->addWidget(coachLabel, 0, 1); + lay->addWidget(coachView, 1, 1); + + QListView *freightView = new QListView; + freightView->setModel(freightModel); + QLabel *freightLabel = new QLabel(tr("Freight Wagons")); + freightLabel->setAlignment(Qt::AlignCenter); + freightLabel->setFont(f); + lay->addWidget(freightLabel, 0, 2); + lay->addWidget(freightView, 1, 2); + + showHideLegendBut = new QPushButton; + lay->addWidget(showHideLegendBut, 2, 0, 1, 1); + + legend = new QFrame; + lay->addWidget(legend, 3, 0, 1, 3); + legend->hide(); + + QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Ok); //TODO: implement also cancel + connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject); + lay->addWidget(box, 4, 0, 1, 3); + + connect(showHideLegendBut, &QPushButton::clicked, this, &RSCoupleDialog::toggleLegend); + updateButText(); + + setMinimumSize(400, 300); + setWindowFlag(Qt::WindowMaximizeButtonHint); +} + +void RSCoupleDialog::loadProxyModels(sqlite3pp::database& db, db_id jobId, db_id stopId, db_id stationId, const QTime& arrival) +{ + QVector engines, freight, coaches; + + sqlite3pp::query q(db); + + if(op == Coupled) + { + /* Show Couple-able RS: + * - RS free in this station + * - Unused RS (green) + * - RS without operations before this time (=arrival) (cyan) + * + * Plus: + * - Possible wrong operations to let the user remove them + */ + + q.prepare("SELECT MAX(sub.p), sub.rsId, rs_list.number, rs_models.name, rs_models.suffix, rs_models.type, rs_models.sub_type, sub.arr, sub.jobId, jobs.category FROM (" + + //Select possible wrong operations to let user remove (un-check) them + " SELECT 1 AS p, coupling.rsId AS rsId, NULL AS arr, NULL AS jobId FROM coupling WHERE coupling.stopId=?3 AND coupling.operation=1" + " UNION ALL" + + //Select RS uncoupled before our arrival (included RS uncoupled at exact same time) (except uncoupled by us) + " SELECT 2 AS p, coupling.rsId AS rsId, MAX(stops.arrival) AS arr, stops.jobId AS jobId" + " FROM stops" + " JOIN coupling ON coupling.stopId=stops.id" + " WHERE stops.stationId=?1 AND stops.arrival <= ?2 AND stops.id<>?3" + " GROUP BY coupling.rsId" + " HAVING coupling.operation=0" + " UNION ALL" + + //Select RS coupled after our arrival (excluded RS coupled at exact same time) + " SELECT 3 AS p, coupling.rsId, MIN(stops.arrival) AS arr, stops.jobId AS jobId" + " FROM coupling" + " JOIN stops ON stops.id=coupling.stopId" + " WHERE stops.stationId=?1 AND stops.arrival > ?2" + " GROUP BY coupling.rsId" + " HAVING coupling.operation=1" + " UNION ALL" + + //Select coupled RS for first time + " SELECT 4 AS p, rs_list.id AS rsId, NULL AS arr, NULL AS jobId" + " FROM rs_list" + " WHERE NOT EXISTS (" + " SELECT coupling.rsId FROM coupling" + " JOIN stops ON stops.id=coupling.stopId WHERE coupling.rsId=rs_list.id AND stops.arrival(0); + item.rsId = rs.get(1); + + int number = rs.get(2); + int modelNameLen = sqlite3_column_bytes(q.stmt(), 3); + const char *modelName = reinterpret_cast(sqlite3_column_text(q.stmt(), 3)); + + int modelSuffixLen = sqlite3_column_bytes(q.stmt(), 4); + const char *modelSuffix = reinterpret_cast(sqlite3_column_text(q.stmt(), 4)); + RsType type = RsType(rs.get(5)); + RsEngineSubType subType = RsEngineSubType(rs.get(6)); + + item.rsName = rs_utils::formatNameRef(modelName, modelNameLen, + number, + modelSuffix, modelSuffixLen, + type); + + item.engineType = RsEngineSubType::Invalid; + + item.time = rs.get(7); + item.jobId = rs.get(8); + item.jobCat = JobCategory(rs.get(9)); + + switch (type) + { + case RsType::Engine: + { + item.engineType = subType; + engines.append(item); + break; + } + case RsType::FreightWagon: + { + freight.append(item); + break; + } + case RsType::Coach: + { + coaches.append(item); + break; + } + default: + break; + } + } + + engModel->loadData(engines); + freightModel->loadData(freight); + coachModel->loadData(coaches); +} + +void RSCoupleDialog::toggleLegend() +{ + if(legend->isVisible()) + { + legend->hide(); + }else{ + legend->show(); + if(!legend->layout()) + { + QVBoxLayout *legendLay = new QVBoxLayout(legend); + QLabel *label = new QLabel(tr("

" + "___ The item isn't coupled before or already coupled.
" + "___ The item isn't in this station.
" + "\\\\\\\\ Railway line doesn't allow electric traction.
" + "___ First use of this item.
" + "___ This item is never used in this session.

")); + label->setTextFormat(Qt::RichText); + legendLay->addWidget(label); + legend->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + } + } + updateButText(); + adjustSize(); +} + +void RSCoupleDialog::updateButText() +{ + showHideLegendBut->setText(legend->isVisible() ? tr("Hide legend") : tr("Show legend")); +} diff --git a/src/jobs/jobeditor/rscoupledialog.h b/src/jobs/jobeditor/rscoupledialog.h new file mode 100644 index 0000000..ba9f6f1 --- /dev/null +++ b/src/jobs/jobeditor/rscoupledialog.h @@ -0,0 +1,44 @@ +#ifndef RSCOUPLEDIALOG_H +#define RSCOUPLEDIALOG_H + +#include + +#include "utils/types.h" + +class RSCouplingInterface; +class RSProxyModel; +class QPushButton; + +namespace sqlite3pp { +class database; +} + +//FIXME: on-demand load and filter +class RSCoupleDialog : public QDialog +{ + Q_OBJECT +public: + RSCoupleDialog(RSCouplingInterface *mgr, RsOp o, QWidget *parent = nullptr); + + void loadProxyModels(sqlite3pp::database &db, db_id jobId, db_id stopId, db_id stationId, const QTime &arrival); + +private slots: + void toggleLegend(); + +private: + void updateButText(); + +private: + RSCouplingInterface *couplingMgr; + + RSProxyModel *engModel; + RSProxyModel *coachModel; + RSProxyModel *freightModel; + + QPushButton *showHideLegendBut; + QWidget *legend; + + RsOp op; +}; + +#endif // RSCOUPLEDIALOG_H diff --git a/src/jobs/jobeditor/shiftbusy/CMakeLists.txt b/src/jobs/jobeditor/shiftbusy/CMakeLists.txt new file mode 100644 index 0000000..883863b --- /dev/null +++ b/src/jobs/jobeditor/shiftbusy/CMakeLists.txt @@ -0,0 +1,10 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + + jobs/jobeditor/shiftbusy/shiftbusymodel.h + jobs/jobeditor/shiftbusy/shiftbusydialog.h + + jobs/jobeditor/shiftbusy/shiftbusymodel.cpp + jobs/jobeditor/shiftbusy/shiftbusydialog.cpp + PARENT_SCOPE +) diff --git a/src/jobs/jobeditor/shiftbusy/shiftbusydialog.cpp b/src/jobs/jobeditor/shiftbusy/shiftbusydialog.cpp new file mode 100644 index 0000000..421c798 --- /dev/null +++ b/src/jobs/jobeditor/shiftbusy/shiftbusydialog.cpp @@ -0,0 +1,45 @@ +#include "shiftbusydialog.h" +#include "shiftbusymodel.h" + +#include + +#include +#include +#include + +#include + +ShiftBusyDlg::ShiftBusyDlg(QWidget *parent) : + QDialog(parent), + model(nullptr) +{ + QVBoxLayout *lay = new QVBoxLayout(this); + + m_label = new QLabel; + lay->addWidget(m_label); + + view = new QTableView; + lay->addWidget(view); + + QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Ok); + connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject); + lay->addWidget(box); + + setWindowTitle(tr("Shift is busy")); + setMinimumSize(350, 200); +} + +void ShiftBusyDlg::setModel(ShiftBusyModel *m) +{ + model = m; + view->setModel(m); + + m_label->setText(tr("Cannot set shift %1 to job %2.
" + "The selected shift is busy:
" + "From: %3 To: %4") + .arg(model->getShiftName()) + .arg(model->getJobName()) + .arg(model->getStart().toString("HH:mm")) + .arg(model->getEnd().toString("HH:mm"))); +} diff --git a/src/jobs/jobeditor/shiftbusy/shiftbusydialog.h b/src/jobs/jobeditor/shiftbusy/shiftbusydialog.h new file mode 100644 index 0000000..f04af03 --- /dev/null +++ b/src/jobs/jobeditor/shiftbusy/shiftbusydialog.h @@ -0,0 +1,27 @@ +#ifndef SHIFTBUSYBOX_H +#define SHIFTBUSYBOX_H + +#include + +#include + +class QLabel; +class QTableView; + +class ShiftBusyModel; + +class ShiftBusyDlg : public QDialog +{ + Q_OBJECT +public: + explicit ShiftBusyDlg(QWidget *parent = nullptr); + + void setModel(ShiftBusyModel *m); +private: + QLabel *m_label; + QTableView *view; + + ShiftBusyModel *model; +}; + +#endif // SHIFTBUSYBOX_H diff --git a/src/jobs/jobeditor/shiftbusy/shiftbusymodel.cpp b/src/jobs/jobeditor/shiftbusy/shiftbusymodel.cpp new file mode 100644 index 0000000..513c934 --- /dev/null +++ b/src/jobs/jobeditor/shiftbusy/shiftbusymodel.cpp @@ -0,0 +1,145 @@ +#include "shiftbusymodel.h" +#include "utils/model_roles.h" + +#include "utils/jobcategorystrings.h" + +#include +using namespace sqlite3pp; + +ShiftBusyModel::ShiftBusyModel(sqlite3pp::database &db, QObject *parent) : + QAbstractTableModel(parent), + mDb(db), + m_shiftId(0), + m_jobId(0), + m_jobCat(JobCategory::FREIGHT) +{ +} + +QVariant ShiftBusyModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch (section) { + case JobCol: + return tr("Job"); + case Start: + return tr("From"); + case End: + return tr("To"); + } + } + + return QAbstractTableModel::headerData(section, orientation, role); +} + +int ShiftBusyModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_data.size(); +} + +int ShiftBusyModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant ShiftBusyModel::data(const QModelIndex &idx, int role) const +{ + const int row = idx.row(); + if (!idx.isValid() || row >= m_data.size() || idx.column() >= NCols) + return QVariant(); + + const JobInfo& info = m_data.at(row); + + switch (role) + { + case Qt::DisplayRole: + { + switch (idx.column()) + { + case JobCol: + return JobCategoryName::jobName(info.jobId, info.jobCat); + case Start: + return info.start; + case End: + return info.end; + } + break; + } + case JOB_ID_ROLE: + { + return info.jobId; + } + } + + return QVariant(); +} + +void ShiftBusyModel::loadData(db_id shiftId, db_id jobId, const QTime &start, const QTime &end) +{ + beginResetModel(); + m_data.clear(); + + m_shiftId = shiftId; + m_jobId = jobId; + m_start = start; + m_end = end; + + query q(mDb, "SELECT name FROM jobshifts WHERE id=?"); + q.bind(1, m_shiftId); + if(q.step() != SQLITE_ROW) + { + endResetModel(); + return; + } + + m_shiftName = q.getRows().get(0); + + q.prepare("SELECT COUNT(1)" + " FROM jobs" + " JOIN stops s1 ON s1.id=jobs.firstStop" + " JOIN stops s2 ON s2.id=jobs.lastStop" + " WHERE jobs.shiftId=? AND s2.departure>? AND s1.arrival(0) - 1; //Do not count ourself + m_data.reserve(count); + + q.prepare("SELECT jobs.id,jobs.category," + " s1.arrival," + " s2.departure" + " FROM jobs" + " JOIN stops s1 ON s1.id=jobs.firstStop" + " JOIN stops s2 ON s2.id=jobs.lastStop" + " WHERE jobs.shiftId=? AND s2.departure>? AND s1.arrival(0); + info.jobCat = JobCategory(j.get(1)); + + if(info.jobId == m_jobId) + { + m_jobCat = info.jobCat; + continue; + } + + info.start = j.get(3); + info.end = j.get(4); + + m_data.append(info); + } + + endResetModel(); +} + +QString ShiftBusyModel::getJobName() const +{ + return JobCategoryName::jobName(m_jobId, m_jobCat); +} diff --git a/src/jobs/jobeditor/shiftbusy/shiftbusymodel.h b/src/jobs/jobeditor/shiftbusy/shiftbusymodel.h new file mode 100644 index 0000000..a428b08 --- /dev/null +++ b/src/jobs/jobeditor/shiftbusy/shiftbusymodel.h @@ -0,0 +1,69 @@ +#ifndef SHIFTBUSYMODEL_H +#define SHIFTBUSYMODEL_H + +#include + +#include +#include +#include "utils/types.h" + +namespace sqlite3pp { +class database; +} + +//TODO: move to shifts subdir +class ShiftBusyModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + + typedef enum { + JobCol = 0, + Start, + End, + NCols + } Columns; + + typedef struct + { + db_id jobId; + QTime start; + QTime end; + JobCategory jobCat; + } JobInfo; + + ShiftBusyModel(sqlite3pp::database &db, QObject *parent = nullptr); + + // 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; + + void loadData(db_id shiftId, db_id jobId, const QTime& start, const QTime& end); + + inline bool hasConcurrentJobs() const { return m_data.size(); } + + inline QTime getStart() const { return m_start; } + inline QTime getEnd() const { return m_end; } + + inline QString getShiftName() const { return m_shiftName; } + QString getJobName() const; + +private: + sqlite3pp::database &mDb; + QVector m_data; + + db_id m_shiftId; + db_id m_jobId; + QTime m_start; + QTime m_end; + QString m_shiftName; + JobCategory m_jobCat; +}; + +#endif // SHIFTBUSYMODEL_H diff --git a/src/jobs/jobeditor/stopdelegate.cpp b/src/jobs/jobeditor/stopdelegate.cpp new file mode 100644 index 0000000..0540c54 --- /dev/null +++ b/src/jobs/jobeditor/stopdelegate.cpp @@ -0,0 +1,272 @@ +#include "stopdelegate.h" + +#include "stopeditor.h" + +#include "utils/model_roles.h" + +#include + +#include + +#include "app/session.h" +#include "model/stopmodel.h" + +#include "app/scopedebug.h" + +#include + +StopDelegate::StopDelegate(sqlite3pp::database &db, QObject *parent) : + QStyledItemDelegate(parent), + mDb(db) +{ + renderer = new QSvgRenderer(this); + loadIcon(QCoreApplication::instance()->applicationDirPath() + QStringLiteral("/icons/lightning.svg")); +} + +void StopDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QRect rect = option.rect.adjusted(5, 5, -5, -5); + + const StopModel *model = static_cast(index.model()); + const StopItem item = model->getItemAt(index.row()); + + query q(mDb, "SELECT name FROM stations WHERE id=?"); + q.bind(1, item.stationId); + q.step(); + QString station = q.getRows().get(0); + q.reset(); + + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + + //Draw bottom border + painter->setPen(QPen(Qt::black, 1)); + painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight()); + + if (option.state & QStyle::State_Selected) + painter->fillRect(option.rect, option.palette.highlight()); + + + painter->setPen(QPen(option.palette.text(), 3)); + + QFont font = option.font; + font.setPointSize(12); + font.setBold(item.type != StopType::Transit); //TransitLineChange is bold too to notify the line change + + painter->setFont(font); + + painter->setBrush(option.palette.text()); + + const bool isTransit = (item.type == StopType::Transit || item.type == StopType::TransitLineChange); + + const double top = rect.top(); + const double bottom = rect.bottom(); + const double left = rect.left(); + const double width = rect.width(); + const double height = rect.height(); + + const double stHeight = top + (isTransit ? 0.0 : height * 0.1); + const double timeHeight = top + height * (item.type == TransitLineChange ? 0.3 : 0.4); + const double lineHeight = top + height * (isTransit ? 0.6 : 0.7); + + const double arrX = left + width * (isTransit ? 0.4 : 0.2); + const double depX = left + width * 0.6; + const double lineX = left + (isTransit ? width * 0.1 : 0.0); + + + int addHere = index.data(ADDHERE_ROLE).toInt(); + if(addHere == 0) + { + //Draw item + //Station name + painter->drawText(QRectF(left, stHeight, width, bottom - stHeight), + station, + QTextOption(Qt::AlignHCenter)); + + if(item.type != First) + { + //Arrival + painter->drawText(QRectF(arrX, timeHeight, width, bottom - timeHeight), + item.arrival.toString("HH:mm")); + } + + if(item.type == First || item.type == Normal) //Last, Transit, TransitLineChange don't have a separate departure + { + //Departure + painter->drawText(QRectF(depX, timeHeight, width, bottom - timeHeight), + item.departure.toString("HH:mm")); + } + + if(item.nextLine != 0 || item.type == TransitLineChange || item.type == First) + { + q.prepare("SELECT type,name FROM lines WHERE id=?"); + q.bind(1, item.nextLine); + q.step(); + auto r = q.getRows(); + LineType nextLineType = LineType(r.get(0)); + QString lineName = r.get(1); + q.reset(); + + //Line name (on First or on line change) + painter->drawText(QRectF(lineX, lineHeight, width, bottom - lineHeight), + tr("Line: %1").arg(lineName), + QTextOption(Qt::AlignHCenter)); + + if(item.type != Last) + { + LineType oldLineType = LineType(-1); + if(item.type != First) + { + q.bind(1, item.curLine); + q.step(); + oldLineType = LineType(q.getRows().get(0)); + q.reset(); + } + + if(oldLineType != nextLineType) + { + QSizeF s = QSizeF(renderer->defaultSize()).scaled(width, height / 2, Qt::KeepAspectRatio); + QRectF lightningRect(left, top + height / 4, s.width(), s.height()); + renderer->render(painter, lightningRect); + + if(nextLineType != LineType::Electric) + { + painter->setPen(QPen(Qt::red, 4)); + painter->drawLine(lightningRect.topLeft(), lightningRect.bottomRight()); + } + } + } + } + + if(isTransit) + { + painter->setPen(QPen(Qt::red, 5)); + painter->setBrush(Qt::red); + painter->drawLine(QLineF(rect.left() + rect.width() * 0.2, rect.top(), + rect.left() + rect.width() * 0.2, rect.bottom())); + + painter->drawEllipse(QRectF(rect.left() + rect.width() * 0.2 - 12 / 2, + rect.top() + rect.height() * 0.4, + 12, 12)); + } + + } + else if (addHere == 1) + { + painter->drawText(rect, "Add Here", QTextOption(Qt::AlignCenter)); + } + else + { + painter->drawText(rect, "Insert Here", QTextOption(Qt::AlignCenter)); + } + + painter->restore(); +} + +QSize StopDelegate::sizeHint(const QStyleOptionViewItem &/*option*/, + const QModelIndex &index) const +{ + int w = 200; + int h = 100; + StopType t = getStopType(index); + if(t == Transit) + h = 60; + else if(t == TransitLineChange) + h = 80; + if(index.data(ADDHERE_ROLE).toInt() != 0) + h = 30; + return QSize(w, h); +} + +QWidget *StopDelegate::createEditor(QWidget *parent, + const QStyleOptionViewItem &/*option*/, + const QModelIndex &index) const + +{ + StopType type = getStopType(index); + int addHere = index.data(ADDHERE_ROLE).toInt(); + if( addHere != 0) + { + qDebug() << index << "is AddHere"; + return nullptr; + } + + StopEditor *editor = new StopEditor(mDb, parent); + editor->setAutoFillBackground(true); + editor->setStopType(type); + editor->setEnabled(false); //Mark it + + //Prevent JobPathEditor context menu in table view during stop editing + editor->setContextMenuPolicy(Qt::PreventContextMenu); + + //See 'StopEditor::popupLinesCombo' + connect(this, &StopDelegate::popupEditorLinesCombo, editor, &StopEditor::popupLinesCombo); + connect(editor, &StopEditor::lineChosen, this, &StopDelegate::onLineChosen); + + return editor; +} + +void StopDelegate::setEditorData(QWidget *editor, + const QModelIndex &index) const +{ + StopEditor *ed = static_cast(editor); + if(ed->isEnabled()) //We already set data + return; + ed->setEnabled(true); //Mark it + + const StopModel *model = static_cast(index.model()); + const StopItem item = model->getItemAt(index.row()); + + int r = index.row(); + if(r > 0) + { + const StopItem prev = model->getItemAt(r - 1); + ed->setPrevSt(prev.stationId); + ed->setPrevDeparture(prev.departure); + } + + ed->setStation(item.stationId); + ed->setArrival(item.arrival); + ed->setDeparture(item.departure); + ed->setCurLine(item.curLine); + if(item.nextLine) + ed->setNextLine(item.nextLine); + + ed->calcInfo(); +} + +void StopDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const +{ + DEBUG_IMPORTANT_ENTRY; + + qDebug() << "End editing: stop" << index.row(); + StopEditor *ed = static_cast(editor); + model->setData(index, ed->getArrival(), ARR_ROLE); + model->setData(index, ed->getDeparture(), DEP_ROLE); + model->setData(index, ed->getStation(), STATION_ROLE); + + db_id nextLine = ed->getNextLine(); + qDebug() << "NextLine:" << nextLine; + model->setData(index, nextLine, NEXT_LINE_ROLE); +} + +void StopDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &/*index*/) const +{ + editor->setGeometry(option.rect); +} + +void StopDelegate::loadIcon(const QString &fileName) +{ + renderer->load(fileName); +} + +void StopDelegate::onLineChosen(StopEditor *editor) +{ + if(editor->closeOnLineChosen()) + { + commitData(editor); + closeEditor(editor, StopDelegate::EditNextItem); + } +} diff --git a/src/jobs/jobeditor/stopdelegate.h b/src/jobs/jobeditor/stopdelegate.h new file mode 100644 index 0000000..2eaffa6 --- /dev/null +++ b/src/jobs/jobeditor/stopdelegate.h @@ -0,0 +1,60 @@ +#ifndef STOPDELEGATE_H +#define STOPDELEGATE_H + +#include + +#include "utils/model_roles.h" +#include "utils/types.h" + +#include + +namespace sqlite3pp { +class database; +} + +class StopEditor; + +class StopDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + + static inline StopType getStopType(const QModelIndex& idx) + { + return static_cast(idx.data(STOP_TYPE_ROLE).toInt()); + } + + static inline void setStopType(QAbstractItemModel *m, const QModelIndex& idx, StopType type) + { + m->setData(idx, int(type), STOP_TYPE_ROLE); + } + + StopDelegate(sqlite3pp::database &db, QObject *parent = nullptr); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override; + + void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &) const override; + + void loadIcon(const QString& fileName); + +signals: + void popupEditorLinesCombo(); + +private slots: + void onLineChosen(StopEditor *editor); + +private: + QSvgRenderer *renderer; + sqlite3pp::database &mDb; +}; + +#endif // STOPDELEGATE_H diff --git a/src/jobs/jobeditor/stopeditor.cpp b/src/jobs/jobeditor/stopeditor.cpp new file mode 100644 index 0000000..faf98f6 --- /dev/null +++ b/src/jobs/jobeditor/stopeditor.cpp @@ -0,0 +1,375 @@ +#include "stopeditor.h" + +#include "utils/sqldelegate/customcompletionlineedit.h" + +#include "stations/stationsmatchmodel.h" +#include "model/stationlineslistmodel.h" + +#include +#include +#include +#include +#include + +#include + +#include + +#include "app/scopedebug.h" + +StopEditor::StopEditor(sqlite3pp::database &db, QWidget *parent) : + QFrame(parent), + linesModel(nullptr), + curLineId(0), + nextLineId(0), + stopId(0), + stationId(0), + mPrevSt(0), + stopType(Normal), + m_closeOnLineChosen(false) +{ + stationsMatchModel = new StationsMatchModel(db, this); + mLineEdit = new CustomCompletionLineEdit(stationsMatchModel, this); + mLineEdit->setPlaceholderText(tr("Station name")); + + connect(mLineEdit, &CustomCompletionLineEdit::dataIdChanged, this, &StopEditor::onStationSelected); + + arrEdit = new QTimeEdit; + depEdit = new QTimeEdit; + connect(arrEdit, &QTimeEdit::timeChanged, this, &StopEditor::arrivalChanged); +#ifdef PRINT_DBG_MSG + setObjectName(QStringLiteral("StopEditor (%1)").arg(qintptr(this))); + arrEdit->setObjectName(QStringLiteral("QTimeEdit (%1)").arg(qintptr(arrEdit))); + depEdit->setObjectName(QStringLiteral("QTimeEdit (%1)").arg(qintptr(depEdit))); +#endif + linesCombo = new QComboBox; + linesModel = new StationLinesListModel(db, this); + linesCombo->setModel(linesModel); + connect(linesModel, &StationLinesListModel::resultsReady, this, &StopEditor::linesLoaded); + + connect(linesCombo, QOverload::of(&QComboBox::activated), this, &StopEditor::onNextLineChanged); + QAbstractItemView *view = linesCombo->view(); + view->setContextMenuPolicy(Qt::CustomContextMenu); + view->viewport()->installEventFilter(this); + connect(view, &QAbstractItemView::customContextMenuRequested, this, &StopEditor::comboBoxViewContextMenu); + + setFrameShape(QFrame::Box); + + lay = new QGridLayout(this); + lay->addWidget(mLineEdit, 0, 0); + lay->addWidget(arrEdit, 0, 1); + lay->addWidget(depEdit, 0, 2); + lay->addWidget(linesCombo, 1, 0, 1, 3); + + setTabOrder(mLineEdit, arrEdit); + setTabOrder(arrEdit, depEdit); + setTabOrder(depEdit, linesCombo); +} + +bool StopEditor::eventFilter(QObject *watched, QEvent *ev) +{ + //Filter out right click to prevent cobobox popup from closoing when opening context menu + if(ev->type() == QEvent::MouseButtonRelease && watched == linesCombo->view()->viewport()) + { + if(static_cast(ev)->button() == Qt::RightButton) + return true; + } + return false; +} + +void StopEditor::setPrevDeparture(const QTime &prevTime) +{ + /* Next stop must be at least one minute after + * This is to prevent contemporary stops that will break ORDER BY arrival queries */ + const QTime minTime = prevTime.addSecs(60); + arrEdit->blockSignals(true); + arrEdit->setMinimumTime(minTime); + arrEdit->blockSignals(false); + + //If not Transit, at least 1 minute stop, First and Last have the same time for Arrival and Departure + if(stopType == Normal) + depEdit->setMinimumTime(minTime.addSecs(60)); + else + depEdit->setMinimumTime(minTime); +} + +void StopEditor::setPrevSt(db_id stId) +{ + mPrevSt = stId; +} + +void StopEditor::setCurLine(db_id lineId) +{ + curLineId = lineId; + if(nextLineId == 0) + nextLineId = lineId; +} + +void StopEditor::setNextLine(db_id lineId) +{ + nextLineId = lineId; + if(curLineId == 0) + setCurLine(lineId); +} + +void StopEditor::setStopType(int type) +{ + stopType = type; +} + +void StopEditor::calcInfo() +{ + arrEdit->setToolTip(QString()); + switch (stopType) + { + case Normal: + { + arrEdit->setToolTip(tr("Press shift if you don't want to change also departure time.")); + arrEdit->setEnabled(true); + depEdit->setEnabled(true); + break; + } + case Transit: + case TransitLineChange: + { + arrEdit->setEnabled(true); + + depEdit->setEnabled(false); + depEdit->setVisible(false); + break; + } + case First: + { + arrEdit->setEnabled(false); + arrEdit->setVisible(false); + break; + } + case Last: + { + depEdit->setEnabled(false); + depEdit->setVisible(false); + + lay->removeWidget(linesCombo); + linesCombo->hide(); + if(stationId == 0) + setFocusProxy(mLineEdit); + break; + } + default: + break; + } + + mLineEdit->setText(stationsMatchModel->getName(stationId)); + linesModel->setStationId(stationId); + + int row = -1; + if(nextLineId != 0) + { + row = linesModel->getLineRow(nextLineId); + } + if(row < 0) + { + row = linesModel->getLineRow(curLineId); + } + + if(stopType == First) + stationsMatchModel->setFilter(0, 0); + else + stationsMatchModel->setFilter(curLineId, mPrevSt); + + if(row < 0) + row = 0; + linesCombo->setCurrentIndex(row); + + if(stopType != First) + { + //First stop: arrival is hidden, you can change only departure so do not set a minimum + //Normal stop: at least 1 minute stop + //Transit, Last: departure = arrival + QTime minDep = arrEdit->time(); + if(stopType == Normal) + depEdit->setMinimumTime(minDep.addSecs(60)); + else + depEdit->setMinimumTime(minDep); + } +} + +QTime StopEditor::getArrival() +{ + return arrEdit->time(); +} + +QTime StopEditor::getDeparture() +{ + return depEdit->time(); +} + +db_id StopEditor::getCurLine() +{ + return curLineId; +} + +db_id StopEditor::getNextLine() +{ + qDebug() << "Get NextLine:" << nextLineId; + return nextLineId; +} + +db_id StopEditor::getStation() +{ + return stationId; +} + +void StopEditor::popupLinesCombo() +{ + //This code is used when adding a new stop. + //When user clicks on 'AddHere' a new stop is added + //but before editing it, user must choose the line + //that the job will take from former Last Stop. + //(It was Lst Stop before we added this stop, + //so it didn't have a 'next line') + + //1 - We popup lines combo from former last stop + //2 - When user chooses a line we close the editor (emit lineChosen()) + //3 - We edit edit new Last Stop (EditNextItem) + setCloseOnLineChosen(true); + + if(linesCombo->count() == 1) + { + //If there is only one possible 'next line' + //then it is the same as 'current line' so + //it is already set, just tell the delegate + emit lineChosen(this); + } + else + { + //Show available options + linesCombo->showPopup(); + } +} + +void StopEditor::setArrival(const QTime &arr) +{ + arrEdit->blockSignals(true); + arrEdit->setTime(arr); + lastArrival = arr; + arrEdit->blockSignals(false); +} + +void StopEditor::setDeparture(const QTime &dep) +{ + depEdit->setTime(dep); +} + +void StopEditor::setStation(db_id stId) +{ + stationId = stId; +} + +void StopEditor::onStationSelected(db_id stId) +{ + DEBUG_ENTRY; + if(stId <= 0 || stId == stationId) + return; + + stationId = stId; + linesModel->setStationId(stationId); + + /*Try to set ComboBox to old value if it is still present + * Example: + * St: Venezia Line: VE-PD_LL + * + * User changes St to Mestre + * Try to set Combo to 'VE-PD_LL' if new St has this line + * Fallback to CurLine or NextLine or nothing (First index) + */ + int row = linesModel->getLineRow(nextLineId); + if(row < 0) + { + row = linesModel->getLineRow(curLineId); + nextLineId = curLineId; + } + if(row < 0) + { + nextLineId = 0; + } + + if(!curLineId) + row = 0; + + linesCombo->setCurrentIndex(row); +} + +bool StopEditor::closeOnLineChosen() const +{ + return m_closeOnLineChosen; +} + +void StopEditor::setCloseOnLineChosen(bool value) +{ + m_closeOnLineChosen = value; +} + +void StopEditor::onNextLineChanged(int index) +{ + DEBUG_ENTRY; + nextLineId = linesModel->getLineIdAt(index); + qDebug() << "NextLine:" << nextLineId << "(" << index << ")"; + emit lineChosen(this); +} + +void StopEditor::comboBoxViewContextMenu(const QPoint& pos) +{ + int curPage = linesModel->currentPage(); + int pageCount = linesModel->getPageCount(); + if(pageCount < 1) + pageCount = 1; + + QMenu menu(this); + QAction *prevPage = menu.addAction(tr("Previous page")); + QAction *nextPage = menu.addAction(tr("Next page")); + QAction *pageInfo = menu.addAction(tr("Page: %1/%2").arg(curPage + 1).arg(pageCount)); + + prevPage->setEnabled(curPage > 0); + nextPage->setEnabled(curPage < pageCount - 1); + pageInfo->setEnabled(false); //Just info as a label + + QAction *act = menu.exec(linesCombo->view()->viewport()->mapToGlobal(pos)); + if(act == prevPage) + { + linesModel->switchToPage(curPage - 1); + nextLineId = 0; + } + else if(act == nextPage) + { + linesModel->switchToPage(curPage + 1); + nextLineId = 0; + } +} + +void StopEditor::linesLoaded() +{ + if(nextLineId) + return; + nextLineId = linesModel->getLineIdAt(linesCombo->currentIndex()); +} + +void StopEditor::arrivalChanged(const QTime& arrival) +{ + bool shiftPressed = QGuiApplication::keyboardModifiers().testFlag(Qt::ShiftModifier); + QTime dep = depEdit->time(); + if(!shiftPressed) + { + //Shift departure by the same amount if SHIFT NOT pressed + int diff = lastArrival.msecsTo(arrival); + dep = dep.addMSecs(diff); + } + QTime minDep = arrival; + if(stopType == Normal) + { + minDep = arrival.addSecs(60); //At least stop for 1 minute in Normal stops + } + depEdit->setMinimumTime(minDep); + depEdit->setTime(dep); //Set after setting minimum time + lastArrival = arrival; +} diff --git a/src/jobs/jobeditor/stopeditor.h b/src/jobs/jobeditor/stopeditor.h new file mode 100644 index 0000000..7c5bbd0 --- /dev/null +++ b/src/jobs/jobeditor/stopeditor.h @@ -0,0 +1,87 @@ +#ifndef STOPEDITOR_H +#define STOPEDITOR_H + +#include +#include + +#include "utils/types.h" + +class QComboBox; +class CustomCompletionLineEdit; +class QTimeEdit; +class QGridLayout; + +class StationsMatchModel; +class StationLinesListModel; + +namespace sqlite3pp { +class database; +} + +class StopEditor : public QFrame +{ + Q_OBJECT +public: + StopEditor(sqlite3pp::database &db, QWidget *parent = nullptr); + + virtual bool eventFilter(QObject *watched, QEvent *ev) override; + + void setPrevDeparture(const QTime& prevTime); + void setArrival(const QTime& arr); + void setDeparture(const QTime& dep); + void setStation(db_id stId); + void setCurLine(db_id lineId); + void setNextLine(db_id lineId); + void setStopType(int type); + void setPrevSt(db_id stId); + + void calcInfo(); + + QTime getArrival(); + QTime getDeparture(); + db_id getCurLine(); + + db_id getNextLine(); + db_id getStation(); + + bool closeOnLineChosen() const; + void setCloseOnLineChosen(bool value); + +signals: + void lineChosen(StopEditor *ed); + +public slots: + void popupLinesCombo(); + +private slots: + void onStationSelected(db_id stId); + void onNextLineChanged(int index); + + void comboBoxViewContextMenu(const QPoint &pos); + void linesLoaded(); + + void arrivalChanged(const QTime &arrival); + +private: + QGridLayout *lay; + CustomCompletionLineEdit *mLineEdit; + QTimeEdit *arrEdit; + QTimeEdit *depEdit; + QComboBox *linesCombo; + + StationsMatchModel *stationsMatchModel; + StationLinesListModel *linesModel; + + db_id curLineId; + db_id nextLineId; + db_id stopId; + db_id stationId; + db_id mPrevSt; + + QTime lastArrival; + + int stopType; + bool m_closeOnLineChosen; +}; + +#endif // STOPEDITOR_H diff --git a/src/jobs/jobsmanager.cpp b/src/jobs/jobsmanager.cpp new file mode 100644 index 0000000..125c26c --- /dev/null +++ b/src/jobs/jobsmanager.cpp @@ -0,0 +1,104 @@ +#include "jobsmanager.h" + +#include + +#include +#include +#include + +#include "jobssqlmodel.h" +#include "jobstorage.h" +#include "utils/jobcategorystrings.h" + +#include "utils/sqldelegate/modelpageswitcher.h" + +#include "app/session.h" +#include "viewmanager/viewmanager.h" + +#include +#include + +JobsManager::JobsManager(QWidget *parent) : + QWidget(parent) +{ + QVBoxLayout *l = new QVBoxLayout(this); + setMinimumSize(750, 300); + + QToolBar *toolBar = new QToolBar(this); + toolBar->addAction(tr("New Job"), this, &JobsManager::onNewJob); + toolBar->addAction(tr("Remove"), this, &JobsManager::onRemove); + toolBar->addAction(tr("Remove All"), this, &JobsManager::onRemoveAllJobs); + l->addWidget(toolBar); + + view = new QTableView; + connect(view, &QTableView::doubleClicked, this, &JobsManager::onIndexClicked); + l->addWidget(view); + + jobsModel = new JobsSQLModel(Session->m_Db, this); + view->setModel(jobsModel); + + auto ps = new ModelPageSwitcher(false, this); + l->addWidget(ps); + ps->setModel(jobsModel); + //Custom colun sorting + //NOTE: leave disconnect() in the old SIGLAL()/SLOT() version in order to work + QHeaderView *header = view->horizontalHeader(); + disconnect(header, SIGNAL(sectionPressed(int)), view, SLOT(selectColumn(int))); + disconnect(header, SIGNAL(sectionEntered(int)), view, SLOT(_q_selectColumn(int))); + connect(header, &QHeaderView::sectionClicked, this, [this, header](int section) + { + jobsModel->setSortingColumn(section); + header->setSortIndicator(jobsModel->getSortingColumn(), Qt::AscendingOrder); + }); + header->setSortIndicatorShown(true); + header->setSortIndicator(jobsModel->getSortingColumn(), Qt::AscendingOrder); + + jobsModel->refreshData(); + + setWindowTitle("Jobs Manager"); +} + +void JobsManager::onIndexClicked(const QModelIndex& index) +{ + db_id jobId = jobsModel->getIdAtRow(index.row()); + if(!jobId) + return; + Session->getViewManager()->requestJobEditor(jobId); +} + +void JobsManager::onNewJob() +{ + Session->getViewManager()->requestJobCreation(); +} + +void JobsManager::onRemove() +{ + QModelIndex idx = view->currentIndex(); + if(!idx.isValid()) + return; + + db_id jobId = jobsModel->getIdAtRow(idx.row()); + JobCategory jobCat = jobsModel->getShiftAnCatAtRow(idx.row()).second; + QString jobName = JobCategoryName::jobName(jobId, jobCat); + + int ret = QMessageBox::question(this, + tr("Job deletion"), + tr("Are you sure to delete job %1?").arg(jobName), + QMessageBox::Yes | QMessageBox::Cancel); + if(ret == QMessageBox::Yes) + { + if(!Session->mJobStorage->removeJob(jobId)) + { + qWarning() << "Error while deleting job:" << jobId << "from JobManager" << Session->m_Db.error_msg(); + //ERRORMSG: message box or statusbar + } + } +} + +void JobsManager::onRemoveAllJobs() +{ + int ret = QMessageBox::question(this, tr("Delete all jobs?"), + tr("Are you really sure you want to delete all jobs from this session?")); + if(ret == QMessageBox::Yes) + Session->mJobStorage->removeAllJobs(); +} diff --git a/src/jobs/jobsmanager.h b/src/jobs/jobsmanager.h new file mode 100644 index 0000000..236850f --- /dev/null +++ b/src/jobs/jobsmanager.h @@ -0,0 +1,26 @@ +#ifndef JOBSVIEWER_H +#define JOBSVIEWER_H + +#include + +class QTableView; +class JobsSQLModel; + +class JobsManager : public QWidget +{ + Q_OBJECT +public: + JobsManager(QWidget *parent = nullptr); + +private slots: + void onIndexClicked(const QModelIndex &index); + void onNewJob(); + void onRemove(); + void onRemoveAllJobs(); + +private: + QTableView *view; + JobsSQLModel *jobsModel; +}; + +#endif // JOBSVIEWER_H diff --git a/src/jobs/jobssqlmodel.cpp b/src/jobs/jobssqlmodel.cpp new file mode 100644 index 0000000..b84f33f --- /dev/null +++ b/src/jobs/jobssqlmodel.cpp @@ -0,0 +1,540 @@ +#include "jobssqlmodel.h" +#include "app/session.h" + +#include +#include + +#include +using namespace sqlite3pp; + +#include "utils/worker_event_types.h" +#include "utils/model_roles.h" + +#include "utils/jobcategorystrings.h" + +#include "lines/linestorage.h" +#include "jobs/jobstorage.h" + +#include + +class JobsSQLModelResultEvent : public QEvent +{ +public: + static constexpr Type _Type = Type(CustomEvents::JobsModelResult); + inline JobsSQLModelResultEvent() : QEvent(_Type) {} + + QVector items; + int firstRow; +}; + +JobsSQLModel::JobsSQLModel(sqlite3pp::database &db, QObject *parent) : + IPagedItemModel(500, db, parent), + cacheFirstRow(0), + firstPendingRow(-BatchSize) +{ + sortColumn = IdCol; + + connect(Session, &MeetingSession::shiftNameChanged, this, &JobsSQLModel::clearCache_slot); + connect(Session, &MeetingSession::shiftJobsChanged, this, &JobsSQLModel::clearCache_slot); + connect(Session->mLineStorage, &LineStorage::stationNameChanged, this, &JobsSQLModel::clearCache_slot); + connect(Session, &MeetingSession::jobChanged, this, &JobsSQLModel::clearCache_slot); + + connect(Session->mJobStorage, &JobStorage::jobAdded, this, &JobsSQLModel::onJobAddedOrRemoved); + connect(Session->mJobStorage, &JobStorage::jobRemoved, this, &JobsSQLModel::onJobAddedOrRemoved); +} + +bool JobsSQLModel::event(QEvent *e) +{ + if(e->type() == JobsSQLModelResultEvent::_Type) + { + JobsSQLModelResultEvent *ev = static_cast(e); + ev->setAccepted(true); + + handleResult(ev->items, ev->firstRow); + + return true; + } + + return QAbstractTableModel::event(e); +} + +QVariant JobsSQLModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(role == Qt::DisplayRole) + { + if(orientation == Qt::Horizontal) + { + switch (section) + { + case IdCol: + return tr("Number"); + case Category: + return tr("Category"); + case ShiftCol: + return tr("Shift"); + case OriginSt: + return tr("Origin"); + case OriginTime: + return tr("Departure"); + case DestinationSt: + return tr("Destination"); + case DestinationTime: + return tr("Arrival"); + default: + break; + } + } + else + { + return section + curPage * ItemsPerPage + 1; + } + } + return IPagedItemModel::headerData(section, orientation, role); +} + +int JobsSQLModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : curItemCount; +} + +int JobsSQLModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant JobsSQLModel::data(const QModelIndex &idx, int role) const +{ + const int row = idx.row(); + if (!idx.isValid() || row >= curItemCount || idx.column() >= NCols) + return QVariant(); + + 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 JobItem& item = cache.at(row - cacheFirstRow); + + switch (role) + { + case Qt::DisplayRole: + { + switch (idx.column()) + { + case IdCol: + return item.jobId; + case Category: + return JobCategoryName::shortName(item.category); + case ShiftCol: + return item.shiftName; + case OriginSt: + return item.origStName; + case OriginTime: + return item.originTime; + case DestinationSt: + return item.destStName; + case DestinationTime: + return item.destTime; + default: + break; + } + break; + } + case Qt::TextAlignmentRole: + { + if(idx.column() == IdCol) + { + return Qt::AlignVCenter + Qt::AlignRight; + } + break; + } + } + + return QVariant(); +} + +void JobsSQLModel::clearCache() +{ + cache.clear(); + cache.squeeze(); + cacheFirstRow = 0; +} + +void JobsSQLModel::refreshData() +{ + if(!mDb.db()) + return; + + emit itemsReady(-1, -1); //Notify we are about to refresh + + //TODO: consider filters + query q(mDb, "SELECT COUNT(1) FROM jobs"); + 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 JobsSQLModel::setSortingColumn(int col) +{ + if(sortColumn == col || col == OriginSt || col == DestinationSt || col >= NCols) + return; + + clearCache(); + sortColumn = col; + + QModelIndex first = index(0, 0); + QModelIndex last = index(curItemCount - 1, NCols - 1); + emit dataChanged(first, last); +} + +void JobsSQLModel::clearCache_slot() +{ + clearCache(); + QModelIndex start = index(0, 0); + QModelIndex end = index(curItemCount, NCols); + emit dataChanged(start, end); +} + +void JobsSQLModel::onJobAddedOrRemoved() +{ + refreshData(); //Recalc row count +} + +void JobsSQLModel::fetchRow(int row) +{ + if(!mDb.db()) + return; + + 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; + + + //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 JobsSQLModel::internalFetch(int first, int sortCol, int valRow, const QVariant &val) +{ + query q(mDb); + + query q_stationName(mDb, "SELECT name FROM stations WHERE id=?"); + + 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 = nullptr; + + QByteArray sql = "SELECT jobs.id, jobs.category, jobs.shiftId, s.name," + "s1.departure, s1.stationId, s2.arrival, s2.stationId" + " FROM jobs" + " LEFT JOIN stops s1 ON s1.id=jobs.firstStop" + " LEFT JOIN stops s2 ON s2.id=jobs.lastStop" + " LEFT JOIN jobshifts s ON s.id=jobs.shiftId"; + + switch (sortCol) + { + case IdCol: + { + whereCol = "jobs.id"; //Order by 1 column, no where clause + break; + } + case Category: + { + whereCol = "jobs.category,jobs.id"; + break; + } + case ShiftCol: + { + whereCol = "s.name,s1.departure,jobs.id"; + break; + } + case OriginTime: + { + whereCol = "s1.departure,jobs.id"; + break; + } + case DestinationTime: + { + whereCol = "s2.arrival,jobs.id"; + break; + } + } + + if(val.isValid()) + { + sql += " WHERE "; + sql += whereCol; + if(reverse) + 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); + + // if(val.isValid()) + // { + // switch (sortCol) + // { + // case LineNameCol: + // { + // q.bind(3, val.toString()); + // break; + // } + // } + // } + + QVector vec(BatchSize); + + //QString are implicitly shared, use QHash to temporary store them instead + //of creating new ones for each JobItem + QHash shiftHash; + QHash stationHash; + + auto it = q.begin(); + const auto end = q.end(); + + if(reverse) + { + int i = BatchSize - 1; + + for(; it != end; ++it) + { + auto r = *it; + JobItem &item = vec[i]; + item.jobId = r.get(0); + item.category = JobCategory(r.get(1)); + item.shiftId = r.get(2); + + if(item.shiftId) + { + auto shift = shiftHash.constFind(item.shiftId); + if(shift == shiftHash.constEnd()) + { + shift = shiftHash.insert(item.shiftId, r.get(3)); + } + item.shiftName = shift.value(); + } + + item.originTime = r.get(4); + item.originStId = r.get(5); + item.destTime = r.get(6); + item.destStId = r.get(7); + + if(item.originStId) + { + auto st = stationHash.constFind(item.originStId); + if(st == stationHash.constEnd()) + { + q_stationName.bind(1, item.originStId); + q_stationName.step(); + st = stationHash.insert(item.originStId, q_stationName.getRows().get(0)); + q_stationName.reset(); + } + item.origStName = st.value(); + } + + if(item.destStId) + { + auto st = stationHash.constFind(item.destStId); + if(st == stationHash.constEnd()) + { + q_stationName.bind(1, item.destStId); + q_stationName.step(); + st = stationHash.insert(item.destStId, q_stationName.getRows().get(0)); + q_stationName.reset(); + } + item.destStName = st.value(); + } + + i--; + } + if(i > -1) + vec.remove(0, i + 1); + } + else + { + int i = 0; + + for(; it != end; ++it) + { + auto r = *it; + JobItem &item = vec[i]; + item.jobId = r.get(0); + item.category = JobCategory(r.get(1)); + item.shiftId = r.get(2); + + if(item.shiftId) + { + auto shift = shiftHash.constFind(item.shiftId); + if(shift == shiftHash.constEnd()) + { + shift = shiftHash.insert(item.shiftId, r.get(3)); + } + item.shiftName = shift.value(); + } + + item.originTime = r.get(4); + item.originStId = r.get(5); + item.destTime = r.get(6); + item.destStId = r.get(7); + + if(item.originStId) + { + auto st = stationHash.constFind(item.originStId); + if(st == stationHash.constEnd()) + { + q_stationName.bind(1, item.originStId); + q_stationName.step(); + st = stationHash.insert(item.originStId, q_stationName.getRows().get(0)); + q_stationName.reset(); + } + item.origStName = st.value(); + } + + if(item.destStId) + { + auto st = stationHash.constFind(item.destStId); + if(st == stationHash.constEnd()) + { + q_stationName.bind(1, item.destStId); + q_stationName.step(); + st = stationHash.insert(item.destStId, q_stationName.getRows().get(0)); + q_stationName.reset(); + } + item.destStName = st.value(); + } + + i++; + } + if(i < BatchSize) + vec.remove(i, BatchSize - i); + } + + + JobsSQLModelResultEvent *ev = new JobsSQLModelResultEvent; + ev->items = vec; + ev->firstRow = first; + + qApp->postEvent(this, ev); +} + +void JobsSQLModel::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 first" << 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 last" << 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 itemsReady(firstRow, lastRow); + + qDebug() << "TOTAL: From:" << cacheFirstRow << "To:" << cacheFirstRow + cache.size() - 1; +} diff --git a/src/jobs/jobssqlmodel.h b/src/jobs/jobssqlmodel.h new file mode 100644 index 0000000..29b97c1 --- /dev/null +++ b/src/jobs/jobssqlmodel.h @@ -0,0 +1,103 @@ +#ifndef JOBSSQLMODEL_H +#define JOBSSQLMODEL_H + +#include "utils/sqldelegate/pageditemmodel.h" + +#include "utils/types.h" + +#include + +#include + +class JobsSQLModel : public IPagedItemModel +{ + Q_OBJECT +public: + enum { BatchSize = 100 }; + + typedef enum { + IdCol = 0, + Category, + ShiftCol, + OriginSt, + OriginTime, + DestinationSt, + DestinationTime, + NCols + } Columns; + + typedef struct JobItem_ + { + db_id jobId; + db_id shiftId; + db_id originStId; + db_id destStId; + QTime originTime; + QTime destTime; + QString shiftName; + QString origStName; + QString destStName; + JobCategory category; + } JobItem; + + + JobsSQLModel(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; + + // Convinience + inline db_id getIdAtRow(int row) const + { + if (row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return 0; //Invalid + + const JobItem& item = cache.at(row - cacheFirstRow); + return item.jobId; + } + + inline QPair getShiftAnCatAtRow(int row) const + { + if (row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return {0, JobCategory::NCategories}; //Invalid + + const JobItem& item = cache.at(row - cacheFirstRow); + return {item.shiftId, item.category}; + } + +private slots: + void clearCache_slot(); + void onJobAddedOrRemoved(); + +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: + QVector cache; + + int cacheFirstRow; + int firstPendingRow; +}; + +#endif // JOBSSQLMODEL_H diff --git a/src/jobs/jobstorage.cpp b/src/jobs/jobstorage.cpp new file mode 100644 index 0000000..0e35105 --- /dev/null +++ b/src/jobs/jobstorage.cpp @@ -0,0 +1,400 @@ +#include "jobstorage.h" + +#include "traingraphics.h" + +#include + +#include + +#include "app/session.h" + +#include + +#include "lines/linestorage.h" + +class JobStoragePrivate +{ +public: + typedef QHash Data; + Data m_data; +}; + +JobStorage::JobStorage(sqlite3pp::database &db, QObject *parent) : + QObject(parent), + mDb(db) +{ + impl = new JobStoragePrivate; + connect(&AppSettings, &TrainTimetableSettings::jobColorsChanged, this, &JobStorage::updateJobColors); + connect(Session, &MeetingSession::jobChanged, this, &JobStorage::updateJob); +} + +JobStorage::~JobStorage() +{ + clear(); + delete impl; + impl = nullptr; +} + +void JobStorage::fixJobs() +{ + if(!mDb.db()) + return; + + query q(mDb, "SELECT id FROM jobs WHERE firstStop IS NULL OR lastStop IS NULL OR firstStop=lastStop"); + query q_countStops(mDb, "SELECT COUNT(id) FROM stops WHERE jobId=?"); + + for(auto r : q) + { + db_id jobId = r.get(0); + q_countStops.bind(1, jobId); + q_countStops.step(); + int nStops = q_countStops.getRows().get(0); + q_countStops.reset(); + if(nStops < 2) + { + //Invalid job, jobs must have at least 2 stops (start, end) + removeJob(jobId); + } + else + { + updateFirstLast(jobId); + } + } +} + +void JobStorage::clear() +{ + for(TrainGraphics &tg : impl->m_data) + tg.clear(); + impl->m_data.clear(); + impl->m_data.squeeze(); +} + +bool JobStorage::addJob(db_id *outJobId) +{ + command q_newJob(mDb, "INSERT INTO jobs(id,category,firstStop,lastStop, shiftId) VALUES(NULL,0,NULL,NULL,NULL)"); + + sqlite3_mutex *mutex = sqlite3_db_mutex(mDb.db()); + sqlite3_mutex_enter(mutex); + int rc = q_newJob.execute(); + db_id jobId = mDb.last_insert_rowid(); + sqlite3_mutex_leave(mutex); + q_newJob.reset(); + + if(rc != SQLITE_OK) + { + qWarning() << rc << mDb.error_msg(); + if(outJobId) + *outJobId = 0; + return false; + } + + if(outJobId) + *outJobId = jobId; + + emit jobAdded(jobId); + + return true; +} + +bool JobStorage::removeJob(db_id jobId) +{ + query q(mDb, "SELECT shiftId FROM jobs WHERE id=?"); + q.bind(1, jobId); + if(q.step() != SQLITE_ROW) + { + return false; //Job doesn't exist + } + db_id shiftId = q.getRows().get(0); + q.reset(); + + //TODO: inform RS and stations + emit aboutToRemoveJob(jobId); + + if(shiftId != 0) + { + //Remove job from shift + emit Session->shiftJobsChanged(shiftId, jobId); + } + + mDb.execute("BEGIN TRANSACTION"); + + q.prepare("DELETE FROM stops WHERE jobId=?"); + + q.bind(1, jobId); + int ret = q.step(); + q.reset(); + + if(ret == SQLITE_OK || ret == SQLITE_DONE) + { + q.prepare("DELETE FROM jobsegments WHERE jobId=?"); + q.bind(1, jobId); + ret = q.step(); + q.reset(); + } + if(ret == SQLITE_OK || ret == SQLITE_DONE) + { + q.prepare("DELETE FROM jobs WHERE id=?"); + q.bind(1, jobId); + ret = q.step(); + q.reset(); + } + + if(ret == SQLITE_OK || ret == SQLITE_DONE) + { + mDb.execute("COMMIT"); + } + else + { + qDebug() << "Error while removing Job:" << jobId << ret << mDb.error_msg() << mDb.extended_error_code(); + mDb.execute("ROLLBACK"); + return false; + } + + auto it = impl->m_data.find(jobId); + if(it != impl->m_data.end()) + { + TrainGraphics &tg = it.value(); + tg.clear(); + impl->m_data.erase(it); + } + + emit jobRemoved(jobId); + + return true; +} + +void JobStorage::removeAllJobs() +{ + //Old + command cmd(mDb, "DELETE FROM old_coupling"); + cmd.execute(); + + cmd.prepare("DELETE FROM old_stops"); + cmd.execute(); + + cmd.prepare("DELETE FROM old_jobsegments"); + cmd.execute(); + + //Current + cmd.prepare("DELETE FROM coupling"); + cmd.execute(); + + cmd.prepare("DELETE FROM stops"); + cmd.execute(); + + cmd.prepare("DELETE FROM jobsegments"); + cmd.execute(); + + //Delete jobs + cmd.prepare("DELETE FROM jobs"); + cmd.execute(); + + clear(); + + emit jobRemoved(0); +} + +void JobStorage::drawJobs() +{ + for(auto it = impl->m_data.begin(); it != impl->m_data.end(); ) + { + TrainGraphics &tg = it.value(); + tg.recalcPath(); + if(tg.segments.isEmpty()) + { + //No line of this job is currently loaded so discard job + it->clear(); + it = impl->m_data.erase(it); + }else{ + it++; + } + } +} + +void JobStorage::updateJobColors() +{ + for(TrainGraphics &tg : impl->m_data) + { + tg.updateColor(); + } +} + +void JobStorage::updateJob(db_id newId, db_id oldId) +{ + auto it = impl->m_data.find(oldId); + if(it == impl->m_data.end()) + return; + + if(newId != oldId) + { + TrainGraphics tg = it.value(); //Deep copy + impl->m_data.erase(it); + it = impl->m_data.insert(newId, tg); + } + + query q_getCat(mDb, "SELECT category FROM jobs WHERE id=?"); + q_getCat.bind(1, newId); + q_getCat.step(); + JobCategory cat = JobCategory(q_getCat.getRows().get(0)); + it.value().setId(newId); + it.value().setCategory(cat); +} + +void JobStorage::updateJobPath(db_id jobId) +{ + auto it = impl->m_data.find(jobId); + if(it == impl->m_data.end()) + { + //Give a chance to reload, look for a loaded line + query q(mDb, "SELECT category FROM jobs WHERE id=?"); + q.bind(1, jobId); + int ret = q.step(); + JobCategory cat = JobCategory(q.getRows().get(0)); + if(ret != SQLITE_ROW) + return; + + TrainGraphics tg(jobId, cat); + it = impl->m_data.insert(jobId, tg); + } + + TrainGraphics& tg = it.value(); + tg.recalcPath(); + + if(tg.segments.isEmpty()) + { + //No line of this job is currently loaded so discard job + tg.clear(); + impl->m_data.erase(it); + } +} + +void JobStorage::drawJobs(db_id lineId) +{ + query q_getSegments(mDb, "SELECT id, jobId FROM jobsegments WHERE lineId=?"); + + q_getSegments.bind(1, lineId); + for(auto seg : q_getSegments) + { + db_id segId = seg.get(0); + db_id jobId = seg.get(1); + + auto tg = impl->m_data.find(jobId); + if(tg == impl->m_data.end() || !tg.value().segments.contains(segId)) + continue; + + tg.value().drawSegment(segId, lineId); + } + q_getSegments.reset(); +} + +void JobStorage::loadLine(db_id lineId) +{ + query q_getSegments(mDb, "SELECT id, jobId FROM jobsegments WHERE lineId=? ORDER BY jobId"); + query q_getJobCat(mDb, "SELECT category FROM jobs WHERE id=?"); + + q_getSegments.bind(1, lineId); + for(auto seg : q_getSegments) + { + db_id segId = seg.get(0); + db_id jobId = seg.get(1); + + auto it = impl->m_data.find(jobId); + if(it == impl->m_data.end()) + { + q_getJobCat.bind(1, jobId); + int ret = q_getJobCat.step(); + JobCategory cat = JobCategory(q_getJobCat.getRows().get(0)); + q_getJobCat.reset(); + if(ret != SQLITE_ROW) + continue; + + TrainGraphics tg(jobId, cat); + it = impl->m_data.insert(jobId, tg); + } + + TrainGraphics &ref = it.value(); + ref.createSegment(segId); + } + q_getSegments.reset(); +} + +void JobStorage::unloadLine(db_id lineId) +{ + if(impl->m_data.isEmpty() || !mDb.db()) + return; + + query q_getSegments(mDb, "SELECT id, jobId FROM jobsegments WHERE lineId=? ORDER BY jobId"); + + q_getSegments.bind(1, lineId); + for(auto seg : q_getSegments) + { + db_id segId = seg.get(0); + db_id jobId = seg.get(1); + + auto it = impl->m_data.find(jobId); + if(it == impl->m_data.end()) + continue; + + TrainGraphics &tg = it.value(); + auto segGraph = tg.segments.find(segId); + if(segGraph != tg.segments.end()) + { + segGraph->cleanup(); + tg.segments.erase(segGraph); + } + + //Because we ORDER BY we work on a single job for a few cycles + //Last cycle may have empty job (removed all segments) check every cycle + if(tg.segments.isEmpty()) + { + //Unload job + tg.clear(); + impl->m_data.erase(it); + } + } + q_getSegments.reset(); +} + +void JobStorage::updateFirstLast(db_id jobId) +{ + command q(mDb, "UPDATE jobs" + " SET firstStop=(SELECT id" + " FROM(SELECT id, MIN(departure)" + " FROM stops WHERE jobId=?1)" + ") WHERE id=?1"); + + q.bind(1, jobId); + q.execute(); + q.reset(); + + q.prepare("UPDATE jobs" + " SET lastStop=(SELECT id" + " FROM(SELECT id, MAX(departure)" + " FROM stops WHERE jobId=?1)" + ") WHERE id=?1"); + + + + q.bind(1, jobId); + q.execute(); + q.reset(); +} + +bool JobStorage::selectSegment(db_id jobId, db_id segId, bool select, bool ensureVisible) +{ + auto it = impl->m_data.find(jobId); + if(it == impl->m_data.end()) + return false; + + TrainGraphics &tg = it.value(); + auto seg = tg.segments.constFind(segId); + if(seg == tg.segments.constEnd()) + return false; + + seg.value().item->setSelected(select); + if(ensureVisible) + seg.value().item->ensureVisible(); + + return true; +} diff --git a/src/jobs/jobstorage.h b/src/jobs/jobstorage.h new file mode 100644 index 0000000..514a9d7 --- /dev/null +++ b/src/jobs/jobstorage.h @@ -0,0 +1,58 @@ +#ifndef JOBSMODEL_H +#define JOBSMODEL_H + +#include + +#include "utils/types.h" + +namespace sqlite3pp { +class database; +} + +//TODO: at loading check if 'stops.rw_node' AND 'stops.other_rw_node' ARE correct +// otherwise try to fix them and if not possible tell the user, SEE ALSO: SHOW_WRONG_OTHER_RW_NODE.sql, SHOW_WRONG_RW_NODE.sql, FIX_RW_NODE.sql, FIX_OTHER_RW_NODE_PARTIALLY.sql +class JobStoragePrivate; +class JobStorage : public QObject +{ + Q_OBJECT +public: + + JobStorage(sqlite3pp::database& db, QObject *parent = nullptr); + ~JobStorage(); + + void fixJobs(); + void clear(); + + bool addJob(db_id *outJobId = nullptr); + bool removeJob(db_id jobId); + void removeAllJobs(); + + void drawJobs(); + + void updateJobPath(db_id jobId); + + void updateFirstLast(db_id jobId); + + bool selectSegment(db_id jobId, db_id segId, bool select, bool ensureVisible); + +private: + friend class LineStoragePrivate; + void drawJobs(db_id lineId); + void loadLine(db_id lineId); + void unloadLine(db_id lineId); + +signals: + void jobAdded(db_id jobId); + void aboutToRemoveJob(db_id jobId); + void jobRemoved(db_id jobId); + +private slots: + void updateJobColors(); + void updateJob(db_id newId, db_id oldId); + +private: + JobStoragePrivate *impl; + sqlite3pp::database& mDb; +}; + +#endif // JOBSMODEL_H diff --git a/src/jobs/traingraphics.cpp b/src/jobs/traingraphics.cpp new file mode 100644 index 0000000..4f0784f --- /dev/null +++ b/src/jobs/traingraphics.cpp @@ -0,0 +1,317 @@ +#include "traingraphics.h" +#include "app/session.h" +#include "utils/jobcategorystrings.h" + +#include "app/scopedebug.h" + +#include +#include "graph/graphicsscene.h" +#include "utils/model_roles.h" + +#include + +#include "lines/linestorage.h" + +#include "jobs/jobstorage.h" + +#include "sqlite3pp/sqlite3pp.h" +using namespace sqlite3pp; + +constexpr qreal MSEC_PER_HOUR = 1000 * 60 * 60; + +static qreal timeToNum(const QTime &t) +{ + qreal ret = t.msecsSinceStartOfDay() / MSEC_PER_HOUR; + qDebug() << t << ret; + return ret; +} + +TrainGraphics::TrainGraphics(db_id id, JobCategory c) : + m_jobId(id), + category(c) +{ + +} + +void TrainGraphics::clear() +{ + for(SegmentGraph &seg : segments) + seg.cleanup(); + segments.clear(); + Session->mLineStorage->removeJobStops(m_jobId); +} + +void TrainGraphics::setCategory(JobCategory cat) +{ + if(category == cat) + return; + category = cat; + + updateColor(); +} + +void TrainGraphics::updateColor() +{ + const QColor col = Session->colorForCat(category); + QPen pen(col, Session->jobLineWidth); + QBrush brush(col); + for(SegmentGraph& s : segments) + { + s.item->setPen(pen); + for(auto t : qAsConst(s.texts)) + t->setBrush(brush); + } +} + +//NOTE: redraw job after setting a new id +void TrainGraphics::setId(db_id newId) +{ + //Reset stop labels before jobId is changed otherwise we cannot delete them because id doesn't match anymore + Session->mLineStorage->removeJobStops(m_jobId); + + m_jobId = newId; +} + + +#define MID(a, b) ((a+b)/2) + +/* New methods */ + + +void TrainGraphics::recalcPath() +{ + DEBUG_ENTRY; + + clear(); + + LineStorage *lines = Session->mLineStorage; + + query q_selectSegments(Session->m_Db, "SELECT id, lineId FROM jobsegments WHERE jobId=?"); + q_selectSegments.bind(1, m_jobId); + for(query::iterator it = q_selectSegments.begin(); it != q_selectSegments.end(); ++it) + { + const query::rows &segment = *it; + + db_id segId = segment.get(0); + db_id lineId = segment.get(1); + if(lines->sceneForLine(lineId)) + { + createSegment(segId); + drawSegment(segId, lineId); + } + } +} + +void TrainGraphics::createSegment(db_id segId) +{ + SegmentGraph s(segId); + auto i = new QGraphicsPathItem(); + i->setData(JOB_ID_ROLE, m_jobId); + i->setData(SEGMENT_ROLE, segId); + i->setFlag(QGraphicsItem::ItemIsSelectable); + s.item = i; + + segments.insert(segId, s); +} + +void TrainGraphics::drawSegment(db_id segId, db_id lineId) +{ + DEBUG_ENTRY; + + QString name = JobCategoryName::jobName(m_jobId, category); + + auto seg = segments.find(segId); + if(seg == segments.end()) + return; + + query q_selectSegStops(Session->m_Db, "SELECT id," + "stationId," + "arrival," + "departure," + "platform," + "transit," + "otherSegment" + " FROM stops" + " WHERE segmentId=?1 OR otherSegment=?1" + " ORDER BY arrival ASC"); + + SegmentGraph& s = seg.value(); + s.lineId = lineId; + s.item->setData(LINE_ID, lineId); + + auto scene = Session->mLineStorage->sceneForLine(lineId); + if(!scene) + { + qWarning() << "Err: job:" << m_jobId << "line:" << lineId << "Null Scene!!!"; + return; + } + if(scene != s.item->scene()) + { + scene->addItem(s.item); + } + + QFont labelFont; + labelFont.setPointSizeF(AppSettings.getJobLabelFontSize()); + const QColor col = Session->colorForCat(category); + QPen pen(col, Session->jobLineWidth); + QBrush brush(col); + s.item->setPen(pen); + + int vertOffset = Session->vertOffset; + int hourOffset = Session->hourOffset; + + QVector vec; + QVector angles; + + q_selectSegStops.bind(1, segId); + q_selectSegStops.bind(2, segId); + query::iterator it = q_selectSegStops.begin(); + if(it == q_selectSegStops.end()) + { + qWarning() << "Error: no stops registered for job" << m_jobId; + return; + } + else + { + qInfo() << "Calculatin path of job" << m_jobId; + } + + db_id stopId = (*it).get(0); + db_id stId = (*it).get(1); + QTime arrival = (*it).get(2); + QTime departure = (*it).get(3); + int platf = (*it).get(4); + + qreal hour1 = timeToNum(arrival); + qreal hour2 = timeToNum(departure); + + qDebug() << "Got" << stId << arrival << departure << platf; + + QPainterPath path; + + const qreal x = Session->getStationGraphPos(lineId, stId, platf); + QPointF cur(x, vertOffset + hour2 * hourOffset); + + path.moveTo(cur); + + Session->mLineStorage->addJobStop(stopId, stId, m_jobId, lineId, name, cur.y(), cur.y(), platf); + + ++it; + + for(; it != q_selectSegStops.end(); ++it) + { + stopId = (*it).get(0); + stId = (*it).get(1); + arrival = (*it).get(2); + departure = (*it).get(3); + platf = (*it).get(4); + qDebug() << "Got" << stId << arrival << departure << platf; + + hour1 = timeToNum(arrival); + hour2 = timeToNum(departure); + + QPointF point1(Session->getStationGraphPos(lineId, stId, platf), + vertOffset + hour1 * hourOffset); + QPointF point2(point1.x(), + vertOffset + hour2 * hourOffset); + + Session->mLineStorage->addJobStop(stopId, stId, m_jobId, lineId, name, point1.y(), point2.y(), platf); + + QPointF midPoint = (cur + point1)/2; + + vec << midPoint; + + + path.lineTo(point1); + path.lineTo(point2); + + if(cur.x() < point1.x()) + angles << true; + else + angles << false; + + cur = point2; + } + + s.item->setPath(path); + s.item->setToolTip(name); + + //TODO: Do some heuristics on text positioning + //e.g. Don't write 2 texts if they are too near + // Or write more than 1 if stops are too distant + + const int oldSize = s.texts.size(); + const int size = vec.size(); + + if(oldSize > size) + { + auto iter = s.texts.begin() + size; + auto e = s.texts.end(); + qDeleteAll(iter, e); + s.texts.erase(iter, e); + s.texts.squeeze(); + } + else + { + s.texts.reserve(size); + } + + const int max = qMin(size, oldSize); + for(int i = 0; i < max; i++) + { + QGraphicsSimpleTextItem *ti = s.texts[i]; + if(scene != ti->scene()) + { + scene->addItem(ti); + } + + ti->setBrush(brush); // Don't set QPen + ti->setText(name); + ti->setFont(labelFont); + const QPointF& p = vec[i]; + if(angles[i]) + ti->setPos(p.x(), p.y() - 25); + else + ti->setPos(p.x(), p.y() + 4); + } + + for(int i = oldSize; i < size; i++) + { + QGraphicsSimpleTextItem *ti = scene->addSimpleText(name, + labelFont); + ti->setBrush(brush); // Don't set QPen + ti->setZValue(1); //Labels must be above jobs and platforms + + const QPointF& p = vec[i]; + if(angles[i]) + ti->setPos(p.x(), p.y() - 25); + else + ti->setPos(p.x(), p.y() + 4); + s.texts << ti; + } +} + +SegmentGraph::SegmentGraph(db_id seg, db_id line) : + segId(seg), + lineId(line), + item(nullptr), + texts() +{ + +} + +void SegmentGraph::cleanup() +{ + if(item) + { + //Pevent an automatic selection of an item that will change JobPathEditor + //causing maybeSave() and recursion + //BIG TODO: prevent recursion in JobPathEditor + //Block setJob() while in saveChanges() or discardChanges() + delete item; + item = nullptr; + } + + qDeleteAll(texts); + texts.clear(); +} diff --git a/src/jobs/traingraphics.h b/src/jobs/traingraphics.h new file mode 100644 index 0000000..3c846bb --- /dev/null +++ b/src/jobs/traingraphics.h @@ -0,0 +1,49 @@ +#ifndef TRAINGRAPHICS_H +#define TRAINGRAPHICS_H + +#include + +#include +#include + +#include "utils/types.h" + +class QGraphicsScene; +class QGraphicsPathItem; +class QGraphicsSimpleTextItem; + +class SegmentGraph +{ +public: + SegmentGraph(db_id seg = 0, db_id line = 0); + void cleanup(); + + db_id segId; + db_id lineId; + QGraphicsPathItem *item; + QVector texts; +}; + +class TrainGraphics +{ +public: + TrainGraphics(db_id id, JobCategory c = JobCategory::FREIGHT); + + void setCategory(JobCategory cat); + void updateColor(); + + + db_id m_jobId; + JobCategory category; + + QHash segments; + + void setId(db_id newId); + + void recalcPath(); + void drawSegment(db_id segId, db_id lineId); + void createSegment(db_id segId); + void clear(); +}; + +#endif // TRAINGRAPHICS_H diff --git a/src/lines/CMakeLists.txt b/src/lines/CMakeLists.txt new file mode 100644 index 0000000..3ccc513 --- /dev/null +++ b/src/lines/CMakeLists.txt @@ -0,0 +1,15 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + lines/helpers.h + lines/lineobj.h + lines/linesmatchmodel.h + lines/linessqlmodel.h + lines/linestorage.h + + lines/helpers.cpp + lines/lineobj.cpp + lines/linesmatchmodel.cpp + lines/linessqlmodel.cpp + lines/linestorage.cpp + PARENT_SCOPE +) diff --git a/src/lines/helpers.cpp b/src/lines/helpers.cpp new file mode 100644 index 0000000..029cb33 --- /dev/null +++ b/src/lines/helpers.cpp @@ -0,0 +1,30 @@ +#include "helpers.h" + +#include + +namespace lines { + +double getStationsDistanceInMeters(sqlite3pp::database &db, db_id lineId, db_id stA, db_id stB) +{ + sqlite3pp::query q_getStKmInMeters(db, "SELECT pos_meters FROM railways WHERE lineId=? AND stationId=?"); + q_getStKmInMeters.bind(1, lineId); + q_getStKmInMeters.bind(2, stA); + if(q_getStKmInMeters.step() != SQLITE_ROW) + return 0.0; + const double posA = q_getStKmInMeters.getRows().get(0); + q_getStKmInMeters.reset(); + + q_getStKmInMeters.bind(1, lineId); + q_getStKmInMeters.bind(2, stB); + if(q_getStKmInMeters.step() != SQLITE_ROW) + return 0.0; + const double posB = q_getStKmInMeters.getRows().get(0); + q_getStKmInMeters.reset(); + + const double distance = qAbs(posA - posB); + return distance; +} + +} // namespace lines + + diff --git a/src/lines/helpers.h b/src/lines/helpers.h new file mode 100644 index 0000000..908c648 --- /dev/null +++ b/src/lines/helpers.h @@ -0,0 +1,16 @@ +#ifndef HELPERS_H +#define HELPERS_H + +#include "utils/types.h" + +namespace sqlite3pp { +class database; +} + +namespace lines { + +double getStationsDistanceInMeters(sqlite3pp::database &db, db_id lineId, db_id stA, db_id stB); + +} // namespace lines + +#endif // HELPERS_H diff --git a/src/lines/lineobj.cpp b/src/lines/lineobj.cpp new file mode 100644 index 0000000..6c56ba1 --- /dev/null +++ b/src/lines/lineobj.cpp @@ -0,0 +1,153 @@ +#include "lineobj.h" + +#include "graph/graphicsscene.h" +#include + +#include "app/session.h" + +LineObj::LineObj() : + lineId(0), + scene(new GraphicsScene), + refCount(0) +{ + +} + +StationObj::StationObj() : + stId(0), + platfs(1), + depots(0), + platfRgb(qRgb(255, 255, 255)) +{ + +} + +/* void StationObj::removeJob(db_id jobId) + * Removes all job stops 'reflected' in this station. + * Used when a Job is deleted of if Job Name changed or path is redrawn. +*/ +void StationObj::removeJob(db_id jobId) +{ + for(Graph* g : qAsConst(lineGraphs)) + { + //There are multiple values with same key + auto it = g->stops.find(jobId); + while (it != g->stops.end() && it.key() == jobId) + { + //Cleanup + delete it->line; + delete it->text; + //Remove from list + it = g->stops.erase(it); + } + } +} + +/* void StationObj::addJobStop(db_id stopId, db_id jobId, db_id lineId, const QString &label, qreal y1, qreal y2, int platf) + * Adds a line to platform 'platf' from arrival 'y1' to departure 'y2' + * (Times are already converted in graph cordinates following AppSettings values, + * i.e. vertical offset and hour offset) + * Adds a label with the Job name. + * + * Line and label are added to each line graph containing this line. + * This is used because if a Job stops in a station, its graph is only shown in + * at most 2 line graphs (Current Line, Next Line, if they are the same then just 1 graph) + * + * So to inform other line graph that platform 'platf' is occupied + * we 'reflect' the information in all other line grphs except for lineId (Current Line) +*/ +void StationObj::addJobStop(db_id stopId, db_id jobId, db_id lineId, const QString &label, qreal y1, qreal y2, int platf) +{ + QFont font("Arial", 15, QFont::Bold); //TODO: settings + QBrush brush(Qt::darkGray); //TODO: settings + + QPen p(Qt::darkGray); + p.setWidth(Session->jobLineWidth); + + for(Graph* g : qAsConst(lineGraphs)) + { + if(g->lineId == lineId) + continue; + + //There are multiple values with same key + auto it = g->stops.find(jobId); + while (it != g->stops.end() && it.key() == jobId) + { + if(it->stopId == stopId) + { + //Remove the old stop to avoid duplicates when redrawing the Job. + //Cleanup + delete it->line; + delete it->text; + //Remove from list + it = g->stops.erase(it); + } + else + { + it++; + } + } + + QGraphicsScene *scene = nullptr; + if(!g->platforms.isEmpty()) + scene = g->platforms.first()->scene(); + if(!scene) + return; //Error! + + double x = g->xPos; + double offset = Session->platformOffset; + + if(platf < 0) + { + //Depots are after Platforms, but negative + x += offset * (platfs - platf - 1); + } + else + { + //Main platforms + x += platf * offset; + } + + JobStop s; + s.stopId = stopId; + s.line = scene->addLine(x, y1, x, y2, p); + s.line->setToolTip(label); + + s.text = scene->addSimpleText(label, font); + s.text->setPos(x + 5, y1); + s.text->setZValue(1); //Labels must be above jobs and platforms + s.text->setBrush(brush); + + g->stops.insert(jobId, s); + } +} + +/* void StationObj::setPlatfColor(QRgb rgb) + * Sets a custom color for main platform of this station (in Job Graph) + * and updates the graph in all lines containing this station. + * Depots platform remain unchanged and use default color from AppSettings. + * If rgb is white (255, 255, 255) the main platform color is reset to + * AppSettings default main platform color. + * + * Default value is white so newly created stations use AppSettings default color. +*/ +void StationObj::setPlatfColor(QRgb rgb) +{ + platfRgb = rgb; + + QColor col(platfRgb); + if(platfRgb == qRgb(255, 255, 255)) //If white use default color + col = AppSettings.getMainPlatfColor(); + + QPen pen(col, AppSettings.getPlatformLineWidth()); + + for(Graph* g : qAsConst(lineGraphs)) + { + //Do not alter Depots, just main platforms + const int size = qMin(int(platfs), g->platforms.size()); + for(int i = 0; i < size; i++) + { + g->platforms[i]->setPen(pen); + } + } +} diff --git a/src/lines/lineobj.h b/src/lines/lineobj.h new file mode 100644 index 0000000..6c5382b --- /dev/null +++ b/src/lines/lineobj.h @@ -0,0 +1,73 @@ +#ifndef LINEOBJ_H +#define LINEOBJ_H + +#include + +#include "utils/types.h" + +#include +using namespace sqlite3pp; + +#include + +#include + +class GraphicsScene; +class QGraphicsLineItem; +class QGraphicsSimpleTextItem; + +class StationObj +{ +public: + StationObj(); + + db_id stId = 0; + + qint8 platfs = 1; + qint8 depots = 0; + + QRgb platfRgb; + + class JobStop + { + public: + bool operator==(const JobStop& s) { return this->stopId == s.stopId; } + + db_id stopId; + QGraphicsLineItem *line; + QGraphicsSimpleTextItem *text; + }; + + class Graph + { + public: + qreal xPos = 0.0; + db_id lineId = 0; + db_id stationId = 0; + QVector platforms; + + QMultiHash stops; + }; + + QHash lineGraphs; + + void removeJob(db_id jobId); + void addJobStop(db_id stopId, db_id jobId, db_id lineId, const QString &label, qreal y1, qreal y2, int platf); + + void setPlatfColor(QRgb rgb); +}; + +class LineObj +{ +public: + LineObj(); + + db_id lineId; + GraphicsScene *scene; + + QVector stations; + + int refCount; +}; + +#endif // LINEOBJ_H diff --git a/src/lines/linesmatchmodel.cpp b/src/lines/linesmatchmodel.cpp new file mode 100644 index 0000000..d22d75d --- /dev/null +++ b/src/lines/linesmatchmodel.cpp @@ -0,0 +1,162 @@ +#include "linesmatchmodel.h" + +#include + +LinesMatchModel::LinesMatchModel(database &db, bool useTimer, QObject *parent) : + ISqlFKMatchModel(parent), + mDb(db), + q_getMatches(mDb) +{ + timerId = useTimer ? 0 : -1; +} + +LinesMatchModel::~LinesMatchModel() +{ + if(timerId > 0) + { + killTimer(timerId); + timerId = 0; + } +} + +QVariant LinesMatchModel::data(const QModelIndex &idx, int role) const +{ + if (!idx.isValid() || idx.row() >= size) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + { + if(isEmptyRow(idx.row())) + { + return ISqlFKMatchModel::tr("Empty"); + } + else if(isEllipsesRow(idx.row())) + { + return ellipsesString; + } + + return items[idx.row()].name; + } + case Qt::FontRole: + { + if(isEmptyRow(idx.row())) + { + return boldFont(); + } + break; + } + } + + return QVariant(); +} + +void LinesMatchModel::autoSuggest(const QString &text) +{ + mQuery.clear(); + if(!text.isEmpty()) + { + mQuery.reserve(text.size() + 2); + mQuery.append('%'); + mQuery.append(text.toUtf8()); + mQuery.append('%'); + } + + refreshData(); +} + +void LinesMatchModel::refreshData() +{ + if(!mDb.db()) + return; + + if(!q_getMatches.stmt()) + q_getMatches.prepare("SELECT id,name FROM lines WHERE name LIKE ?1 LIMIT " QT_STRINGIFY(MaxMatchItems + 1)); + + beginResetModel(); + + char emptyQuery = '%'; + + if(mQuery.isEmpty()) + sqlite3_bind_text(q_getMatches.stmt(), 1, &emptyQuery, 1, SQLITE_STATIC); + else + sqlite3_bind_text(q_getMatches.stmt(), 1, mQuery, mQuery.size(), SQLITE_STATIC); + + auto end = q_getMatches.end(); + auto it = q_getMatches.begin(); + int i = 0; + for(; i < MaxMatchItems && it != end; i++) + { + items[i].lineId = (*it).get(0); + items[i].name = (*it).get(1); + ++it; + } + + size = i; + + if(hasEmptyRow) + size++; //Items + Empty, add 1 row + + if(it != end) + { + //There would be still rows, show Ellipses + size++; //Items + Empty + Ellispses + } + + q_getMatches.reset(); + endResetModel(); + + emit resultsReady(false); + + if(timerId < 0) + return; //Do not use timer + if(timerId == 0) + timerId = startTimer(3000); + timer.start(); +} + +void LinesMatchModel::clearCache() +{ + beginResetModel(); + size = 0; + endResetModel(); +} + +QString LinesMatchModel::getName(db_id id) const +{ + if(!mDb.db()) + return QString(); + + query q(mDb, "SELECT name FROM lines WHERE id=?"); + q.bind(1, id); + if(q.step() == SQLITE_ROW) + return q.getRows().get(0); + return QString(); +} + +db_id LinesMatchModel::getIdAtRow(int row) const +{ + return items[row].lineId; +} + +QString LinesMatchModel::getNameAtRow(int row) const +{ + return items[row].name; +} + +void LinesMatchModel::timerEvent(QTimerEvent *e) +{ + if(timerId > 0 && e->timerId() == timerId) + { + if(timer.isValid() && timer.elapsed() < 3000) + return; //Do another round + + killTimer(timerId); + timerId = 0; + q_getMatches.finish(); + return; + } + + ISqlFKMatchModel::timerEvent(e); +} diff --git a/src/lines/linesmatchmodel.h b/src/lines/linesmatchmodel.h new file mode 100644 index 0000000..39cfc71 --- /dev/null +++ b/src/lines/linesmatchmodel.h @@ -0,0 +1,53 @@ +#ifndef LINESMATCHMODEL_H +#define LINESMATCHMODEL_H + +#include "utils/sqldelegate/isqlfkmatchmodel.h" + +#include "utils/types.h" + +#include +using namespace sqlite3pp; + +#include + +class LinesMatchModel : public ISqlFKMatchModel +{ + Q_OBJECT +public: + LinesMatchModel(database &db, bool useTimer, QObject *parent = nullptr); + ~LinesMatchModel(); + + // Basic functionality: + QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override; + + // ISqlFKMatchModel: + void autoSuggest(const QString& text) override; + virtual void refreshData() override; + void clearCache() override; + + QString getName(db_id id) const override; + + db_id getIdAtRow(int row) const override; + QString getNameAtRow(int row) const override; + +protected: + void timerEvent(QTimerEvent *e) override; + +private: + int timerId = 0; + QElapsedTimer timer; + + struct LineItem + { + db_id lineId; + QString name; + char padding[4]; + }; + LineItem items[MaxMatchItems]; + + database &mDb; + query q_getMatches; + QByteArray mQuery; +}; + +#endif // LINESMATCHMODEL_H diff --git a/src/lines/linessqlmodel.cpp b/src/lines/linessqlmodel.cpp new file mode 100644 index 0000000..f145dca --- /dev/null +++ b/src/lines/linessqlmodel.cpp @@ -0,0 +1,592 @@ +#include "linessqlmodel.h" +#include "app/session.h" + +#include +#include + +#include +using namespace sqlite3pp; + +#include "utils/worker_event_types.h" +#include "utils/model_roles.h" + +#include "linestorage.h" + +#include + +class LinesSQLModelResultEvent : public QEvent +{ +public: + static constexpr Type _Type = Type(CustomEvents::LinesModelResult); + inline LinesSQLModelResultEvent() : QEvent(_Type) {} + + QVector items; + int firstRow; +}; + +LinesSQLModel::LinesSQLModel(sqlite3pp::database &db, QObject *parent) : + IPagedItemModel(500, db, parent), + cacheFirstRow(0), + firstPendingRow(-BatchSize) +{ + sortColumn = LineNameCol; + LineStorage *lines = Session->mLineStorage; + connect(lines, &LineStorage::lineAdded, this, &LinesSQLModel::onLineAdded); + connect(lines, &LineStorage::lineRemoved, this, &LinesSQLModel::onLineRemoved); +} + +bool LinesSQLModel::event(QEvent *e) +{ + if(e->type() == LinesSQLModelResultEvent::_Type) + { + LinesSQLModelResultEvent *ev = static_cast(e); + ev->setAccepted(true); + + handleResult(ev->items, ev->firstRow); + + return true; + } + + return QAbstractTableModel::event(e); +} + +QVariant LinesSQLModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(role == Qt::DisplayRole) + { + if(orientation == Qt::Horizontal) + { + switch (section) + { + case LineNameCol: + return tr("Name"); + case LineMaxSpeedKmHCol: + return tr("Max. Speed"); + case LineTypeCol: + return tr("Type"); + default: + break; + } + } + else + { + return section + curPage * ItemsPerPage + 1; + } + } + return IPagedItemModel::headerData(section, orientation, role); +} + +int LinesSQLModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : curItemCount; +} + +int LinesSQLModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant LinesSQLModel::data(const QModelIndex &idx, int role) const +{ + const int row = idx.row(); + if (!idx.isValid() || row >= curItemCount || idx.column() >= NCols) + return QVariant(); + + 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: + { + switch (idx.column()) + { + case LineNameCol: + return item.name; + case LineMaxSpeedKmHCol: + return tr("%1 km/h").arg(item.maxSpeedKmH); + } + break; + } + case Qt::EditRole: + { + switch (idx.column()) + { + case LineNameCol: + return item.name; + case LineMaxSpeedKmHCol: + return item.maxSpeedKmH; + } + break; + } + case Qt::TextAlignmentRole: + { + if(idx.column() == LineMaxSpeedKmHCol) + { + return Qt::AlignVCenter + Qt::AlignRight; + } + break; + } + case Qt::CheckStateRole: + { + if(idx.column() == LineTypeCol) + { + return item.type == LineType::Electric ? Qt::Checked : Qt::Unchecked; + } + break; + } + } + + return QVariant(); +} + +bool LinesSQLModel::setData(const QModelIndex &idx, const QVariant &value, int role) +{ + const int row = idx.row(); + if(!idx.isValid() || row >= curItemCount || idx.column() >= NCols || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + LineItem &item = cache[row - cacheFirstRow]; + QModelIndex first = idx; + QModelIndex last = idx; + + switch (role) + { + case Qt::EditRole: + { + switch (idx.column()) + { + case LineNameCol: + { + const QString newName = value.toString().simplified(); + if(!setName(item, newName)) + return false; + break; + } + case LineMaxSpeedKmHCol: + { + bool ok = false; + int speed = value.toInt(&ok); + if(!ok || !setMaxSpeed(item, speed)) + return false; + break; + } + default: + break; + } + break; + } + case Qt::CheckStateRole: + { + if(idx.column() == LineTypeCol) + { + LineType type = value.value() == Qt::Checked ? LineType::Electric : LineType::NonElectric; + if(!setType(item, type)) + return false; + } + break; + } + default: + break; + } + + emit dataChanged(first, last); + return true; +} + +Qt::ItemFlags LinesSQLModel::flags(const QModelIndex &idx) const +{ + if (!idx.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren; + if(idx.row() < cacheFirstRow || idx.row() >= cacheFirstRow + cache.size()) + return f; //Not fetched yet + + if(idx.column() == LineTypeCol) + f.setFlag(Qt::ItemIsUserCheckable); //LineTypeCol checkbox + else + f.setFlag(Qt::ItemIsEditable); + + return f; +} + +void LinesSQLModel::clearCache() +{ + cache.clear(); + cache.squeeze(); + cacheFirstRow = 0; +} + +void LinesSQLModel::refreshData() +{ + if(!mDb.db()) + return; + + emit itemsReady(-1, -1); //Notify we are about to refresh + + //TODO: consider filters + query q(mDb, "SELECT COUNT(1) FROM lines"); + 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 LinesSQLModel::setSortingColumn(int col) +{ + if(sortColumn == col || col >= NCols) + return; + + clearCache(); + sortColumn = col; + + QModelIndex first = index(0, 0); + QModelIndex last = index(curItemCount - 1, NCols - 1); + emit dataChanged(first, last); +} + +bool LinesSQLModel::removeLine(db_id lineId) +{ + return Session->mLineStorage->removeLine(lineId); +} + +db_id LinesSQLModel::addLine(int *outRow, QString *errOut) +{ + db_id lineId = 0; + if(outRow) + *outRow = 0; + if(!Session->mLineStorage->addLine(&lineId)) + return 0; //Error + + return lineId; +} + +void LinesSQLModel::onLineAdded() +{ + refreshData(); //Recalc row count + setSortingColumn(LineNameCol); + switchToPage(0); //Reset to first page and so it is shown as first row +} + +void LinesSQLModel::onLineRemoved() +{ + refreshData(); //Recalc row count +} + +void LinesSQLModel::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; + + + //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 LinesSQLModel::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 = nullptr; + + QByteArray sql = "SELECT id,name,max_speed,type FROM lines"; + switch (sortCol) + { + case LineNameCol: + { + whereCol = "name"; //Order by 1 column, no where clause + break; + } + case LineMaxSpeedKmHCol: + { + whereCol = "max_speed,name"; //Order by 2 columns, no where clause + break; + } + case LineTypeCol: + { + whereCol = "type,max_speed,name"; //Order by 3 columns, no where clause + break; + } + } + + if(val.isValid()) + { + sql += " WHERE "; + sql += whereCol; + if(reverse) + 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); + + if(val.isValid()) + { + switch (sortCol) + { + case LineNameCol: + { + 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); + item.maxSpeedKmH = r.get(2); + item.type = LineType(r.get(3)); + 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); + item.maxSpeedKmH = r.get(2); + item.type = LineType(r.get(3)); + i++; + } + if(i < BatchSize) + vec.remove(i, BatchSize - i); + } + + + LinesSQLModelResultEvent *ev = new LinesSQLModelResultEvent; + ev->items = vec; + ev->firstRow = first; + + qApp->postEvent(this, ev); +} + +void LinesSQLModel::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 itemsReady(firstRow, lastRow); + + qDebug() << "TOTAL: From:" << cacheFirstRow << "To:" << cacheFirstRow + cache.size() - 1; +} + +bool LinesSQLModel::setName(LinesSQLModel::LineItem &item, const QString &name) +{ + if(item.name == name || name.isEmpty()) + return false; + + command set_name(mDb, "UPDATE lines SET name=? WHERE id=?"); + set_name.bind(1, name); + set_name.bind(2, item.lineId); + int ret = set_name.execute(); + if(ret != SQLITE_OK) + { + qDebug() << "setName()" << ret << mDb.error_code() << mDb.extended_error_code() << mDb.error_msg(); + } + + item.name = name; + + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + + emit Session->mLineStorage->lineNameChanged(item.lineId); + + return true; +} + +bool LinesSQLModel::setMaxSpeed(LinesSQLModel::LineItem &item, int speed) +{ + if(item.maxSpeedKmH == speed || speed < 1 || speed > 9999) + return false; + + command set_speed(mDb, "UPDATE lines SET max_speed=? WHERE id=?"); + set_speed.bind(1, speed); + set_speed.bind(2, item.lineId); + int ret = set_speed.execute(); + if(ret != SQLITE_OK) + { + qDebug() << "setMaxSpeed()" << ret << mDb.error_code() << mDb.extended_error_code() << mDb.error_msg(); + } + + item.maxSpeedKmH = speed; + + if(sortColumn != LineNameCol) + { + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + } + + return true; +} + +bool LinesSQLModel::setType(LinesSQLModel::LineItem &item, LineType type) +{ + if(item.type == type) + return false; + + command set_type(mDb, "UPDATE lines SET type=? WHERE id=?"); + set_type.bind(1, int(type)); + set_type.bind(2, item.lineId); + int ret = set_type.execute(); + if(ret != SQLITE_OK) + { + qDebug() << "setType()" << ret << mDb.error_code() << mDb.extended_error_code() << mDb.error_msg(); + } + + item.type = type; + + if(sortColumn == LineTypeCol) + { + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + } + + return true; +} diff --git a/src/lines/linessqlmodel.h b/src/lines/linessqlmodel.h new file mode 100644 index 0000000..99cc7b1 --- /dev/null +++ b/src/lines/linessqlmodel.h @@ -0,0 +1,103 @@ +#ifndef LINESSQLMODEL_H +#define LINESSQLMODEL_H + +#include "utils/sqldelegate/pageditemmodel.h" + +#include "utils/types.h" + +#include + +class LinesSQLModel : public IPagedItemModel +{ + Q_OBJECT +public: + + enum { BatchSize = 100 }; + + typedef enum { + LineNameCol = 0, + LineMaxSpeedKmHCol, + LineTypeCol, + NCols + } Columns; + + typedef struct LineItem_ + { + db_id lineId; + QString name; + int maxSpeedKmH; + LineType type; + } LineItem; + + LinesSQLModel(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; + + // Editable: + bool setData(const QModelIndex &idx, const QVariant &value, + int role = Qt::EditRole) override; + + Qt::ItemFlags flags(const QModelIndex& idx) 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; + + // LinesSQLModel + bool removeLine(db_id lineId); + db_id addLine(int *outRow, QString *errOut = nullptr); + + // Convinience + inline db_id getIdAtRow(int row) const + { + if (row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return 0; //Invalid + + const LineItem& item = cache.at(row - cacheFirstRow); + return item.lineId; + } + + inline QString getNameAtRow(int row) const + { + if (row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return QString(); //Invalid + + const LineItem& item = cache.at(row - cacheFirstRow); + return item.name; + } + +private slots: + void onLineAdded(); + void onLineRemoved(); + +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); + + bool setName(LineItem &item, const QString &name); + bool setMaxSpeed(LineItem &item, int speed); + bool setType(LineItem &item, LineType type); + +private: + QVector cache; + int cacheFirstRow; + int firstPendingRow; +}; + +#endif // LINESSQLMODEL_H diff --git a/src/lines/linestorage.cpp b/src/lines/linestorage.cpp new file mode 100644 index 0000000..2eb6ea5 --- /dev/null +++ b/src/lines/linestorage.cpp @@ -0,0 +1,645 @@ +#include "linestorage.h" + +#include "app/session.h" + +#include + +#include "lineobj.h" +#include "graph/graphicsscene.h" +#include + +#include "jobs/jobstorage.h" + +#include + +#include + +/* LineStoragePrivate */ + +class LineStoragePrivate +{ +public: + typedef QHash LineLookup; + typedef QHash StationLookup; + + inline LineStoragePrivate(database &db) :mDb(db) {} + + void clear(); + + StationObj *getStation(db_id stationId); + + void addStation(LineObj &line, db_id stId); + void removeStation(LineObj &line, db_id stId); + + void loadLine(LineObj &obj); + LineLookup::iterator unloadLine(LineLookup::iterator it); + + void drawStations(LineObj &line); + void drawJobs(db_id lineId); + +public: + database &mDb; + LineLookup lines; + StationLookup stations; +}; + +void LineStoragePrivate::clear() +{ + //Reset data + for(auto it = lines.begin(); it != lines.end(); ) + { + it = unloadLine(it); + } + lines.clear(); + lines.squeeze(); +} + +StationObj* LineStoragePrivate::getStation(db_id stationId) +{ + auto it = stations.find(stationId); + if(it == stations.end()) + { + //Load station + query q(Session->m_Db, "SELECT id,platforms,depot_platf,platf_color FROM stations WHERE id=?"); + q.bind(1, stationId); + if(q.step() != SQLITE_ROW) + return nullptr; + + auto station = q.getRows(); + StationObj obj; + obj.stId = station.get(0); + obj.platfs = station.get(1); + obj.depots = station.get(2); + + //NULL is white (#FFFFFF) -> default value + if(station.column_type(3) != SQLITE_NULL) + { + obj.platfRgb = QRgb(station.get(3)); + } + + it = stations.insert(obj.stId, obj); + } + return &it.value(); +} + +void LineStoragePrivate::addStation(LineObj &line, db_id stId) +{ + StationObj *st = getStation(stId); + if(!st) + return; + + qreal x = Session->horizOffset; + if(!line.stations.isEmpty()) + { + const StationObj::Graph* prevGraph = line.stations.last(); + auto prevSt = stations.constFind(prevGraph->stationId); + if(prevSt != stations.constEnd()) + { + const int platfCount = prevSt->platfs + prevSt->depots; + x = prevGraph->xPos + Session->platformOffset * platfCount + Session->stationOffset; + } + } + + StationObj::Graph *g = new StationObj::Graph; + g->lineId = line.lineId; + g->stationId = stId; + g->xPos = x; + + st->lineGraphs.insert(line.lineId, g); + line.stations.append(g); +} + +void LineStoragePrivate::removeStation(LineObj &line, db_id stId) +{ + auto st = stations.find(stId); + if(st == stations.end()) + return; //Error! + + auto it = st->lineGraphs.constFind(line.lineId); + if(it == st->lineGraphs.constEnd()) + return; //Station isn't in this line + + const StationObj::Graph* g = it.value(); + qDeleteAll(g->platforms); + + st->lineGraphs.erase(it); + if(st->lineGraphs.isEmpty()) + { + //Unload station + stations.erase(st); + } + + for(int i = 0; i < line.stations.size(); i++) + { + if(line.stations.at(i) == g) + { + line.stations.removeAt(i); + break; + } + } + delete g; +} + +void LineStoragePrivate::loadLine(LineObj &obj) +{ + query q_selectLineStations(mDb, "SELECT stationId FROM railways WHERE lineId=? ORDER BY pos_meters ASC"); + q_selectLineStations.bind(1, obj.lineId); + for(auto r : q_selectLineStations) + { + db_id stId = r.get(0); + addStation(obj, stId); + } + q_selectLineStations.reset(); + + Session->mJobStorage->loadLine(obj.lineId); +} + +LineStoragePrivate::LineLookup::iterator LineStoragePrivate::unloadLine(LineLookup::iterator it) +{ + LineObj &line = it.value(); + + Session->mJobStorage->unloadLine(line.lineId); + + for(StationObj::Graph *g : qAsConst(line.stations)) + { + auto st = stations.find(g->stationId); + Q_ASSERT_X(st != stations.end(), "LineStorage", "Line has ghost station"); + st->lineGraphs.remove(line.lineId); + if(st->lineGraphs.isEmpty()) + { + //Unload station + stations.erase(st); + } + + qDeleteAll(g->platforms); + g->platforms.clear(); + + for(StationObj::JobStop& s : g->stops) + { + delete s.line; + delete s.text; + } + delete g; + } + line.stations.clear(); + delete line.scene; + line.scene = nullptr; + + return lines.erase(it); +} + +void LineStoragePrivate::drawStations(LineObj &line) +{ + const QRgb white = qRgb(255, 255, 255); + qreal x = Session->horizOffset; + db_id lastSt = 0; + const int vertOffset = Session->vertOffset; + const int stationOffset = Session->stationOffset; + const double platfOffset = Session->platformOffset; + const int lastY = vertOffset + Session->hourOffset * 24 + 10; + + const int width = AppSettings.getPlatformLineWidth(); + const QPen mainPlatfPen (AppSettings.getMainPlatfColor(), width); //For main Platfs + const QPen depotPlatfPen(AppSettings.getDepotPlatfColor(), width); //For depots + + const db_id lineId = line.lineId; + QGraphicsScene *scene = line.scene; + + line.stations.clear(); + + query q_selectLineStations(mDb, "SELECT stationId FROM railways WHERE lineId=? ORDER BY pos_meters ASC"); + q_selectLineStations.bind(1, lineId); + for(auto it = q_selectLineStations.begin(); it != q_selectLineStations.end(); ++it) + { + db_id stId = (*it).get(0); + lastSt = stId; + + StationObj *obj = getStation(stId); + if(!obj) + { + //Error, do not assert here + //It could be caused by a wrong insert into railways table + //i.e. someone links a station to this line without telling LineStorage to sync + continue; + } + + auto iter = obj->lineGraphs.find(lineId); + if(iter == obj->lineGraphs.end()) + { + //Error + continue; + } + + StationObj::Graph* graph = iter.value(); + graph->xPos = x; + + line.stations.append(graph); + + //Clear stops, they will be re-inserted when we re-draw jobs + for(StationObj::JobStop& s : graph->stops) + { + delete s.line; + delete s.text; + } + graph->stops.clear(); + + QVector& vec = graph->platforms; + const int oldSize = vec.count(); + const int size = obj->platfs + obj->depots; + + if(oldSize > size) //Delete extra + { + auto platf = vec.begin() + size; + auto e = vec.end(); + qDeleteAll(platf, e); + vec.erase(platf, e); + vec.squeeze(); + } + else + { + vec.reserve(size); + } + + const int max = qMin(size, oldSize); + for(int i = 0; i < max; i++) + { + auto l = vec.at(i); + l->setLine(x, vertOffset, + x, lastY); + x += platfOffset; + } + + for(int i = oldSize; i < size; i++) + { + auto l = scene->addLine(x, vertOffset, + x, lastY); + l->setZValue(-1); //Platform must be below jobs and labels + vec.append(l); + x += platfOffset; + } + + + QPen p = mainPlatfPen; + if(obj->platfRgb != white) //If white (#FFFFFF) then use default color + { + p.setColor(obj->platfRgb); + } + + int i = 0; + for(; i < obj->platfs; i++) //Main Platf + { + auto l = vec.at(i); + l->setPen(p); + } + for(; i < size; i++) //Depots + { + auto l = vec.at(i); + l->setPen(depotPlatfPen); + } + + x += stationOffset; + } + q_selectLineStations.reset(); + + //Leave space for last station label + query q(Session->m_Db, "SELECT name,short_name FROM stations WHERE id=?"); + q.bind(1, lastSt); + auto station = q.getRows(); + QString lastName; + if(station.column_bytes(1) == 0) + lastName = station.get(0); //Fallback to full name + else + lastName = station.get(1); + + QFont f; + f.setBold(true); + f.setPointSize(15); + int nameWidth = QFontMetrics(f).horizontalAdvance(lastName); + + QRectF r = scene->itemsBoundingRect(); + r.setRight(x - stationOffset + nameWidth); + r.adjust(0, 0, 0, 10); + r.setTopLeft(QPointF(0.0, 0.0)); + scene->setSceneRect(r); +} + +void LineStoragePrivate::drawJobs(db_id lineId) +{ + Session->mJobStorage->drawJobs(lineId); +} + +/* LineStorage */ + +LineStorage::LineStorage(database &db, QObject *parent) : + QObject(parent), + mDb(db) +{ + impl = new LineStoragePrivate(db); + connect(this, &LineStorage::stationModified, this, &LineStorage::onStationModified); + connect(this, &LineStorage::stationColorChanged, this, &LineStorage::updateStationColor); +} + +LineStorage::~LineStorage() +{ + delete impl; + impl = nullptr; +} + +void LineStorage::clear() +{ + impl->clear(); +} + +bool LineStorage::addLine(db_id *outLineId) +{ + if(!mDb.db()) + return false; + + sqlite3pp::command q_newLine(mDb, "INSERT INTO lines (id, name, max_speed, type) VALUES(NULL, '', 100, 1)"); + + //TODO: First find possible empty row + + LineObj obj; + sqlite3_mutex *mutex = sqlite3_db_mutex(mDb.db()); + sqlite3_mutex_enter(mutex); + q_newLine.execute(); + obj.lineId = mDb.last_insert_rowid(); + sqlite3_mutex_leave(mutex); + q_newLine.reset(); + + if(obj.lineId == 0) + { + //Error + qDebug() << mDb.error_msg(); + return false; + } + + impl->lines.insert(obj.lineId, obj); + + emit lineAdded(obj.lineId); + + if(outLineId) + *outLineId = obj.lineId; + + return true; +} + +bool LineStorage::removeLine(db_id lineId) +{ + sqlite3pp::command q_removeLine(mDb, "DELETE FROM lines WHERE id=?"); + + //BIG TODO: experimental foreign keys, instead of removing stations from line just block + //Discuss this behaviour: maybe if there are no trains on this line we can remove it directly otherwise block and infor user + + //q_RemoveLineStations.bind(1, lineId); + //q_RemoveLineStations.execute(); + //q_RemoveLineStations.reset(); + + q_removeLine.bind(1, lineId); + int ret = q_removeLine.execute(); + q_removeLine.reset(); + + if(ret != SQLITE_OK) + { + qDebug() << Q_FUNC_INFO << "Error:" << ret << mDb.error_msg(); + int err = mDb.extended_error_code(); + qDebug() << "Extended error:" << err; + if(err == SQLITE_CONSTRAINT_TRIGGER) + qDebug() << "Line" << lineId << "IS IN USE"; + return false; + } + + emit lineAboutToBeRemoved(lineId); + + auto it = impl->lines.find(lineId); + if(it != impl->lines.end()) + { + impl->unloadLine(it); + } + + emit lineRemoved(lineId); + + return true; +} + +bool LineStorage::addStation(db_id *outStationId) +{ + command q_newStation(mDb, "INSERT INTO stations(id,name, short_name,platforms,depot_platf, platf_color)" + " VALUES (NULL, '', NULL, 1, 0, NULL)"); + + sqlite3_mutex *mutex = sqlite3_db_mutex(mDb.db()); + sqlite3_mutex_enter(mutex); + int ret = q_newStation.execute(); + db_id stationId = mDb.last_insert_rowid(); + sqlite3_mutex_leave(mutex); + q_newStation.reset(); + + if(ret != SQLITE_OK || stationId == 0) + { + if(outStationId) + *outStationId = 0; + + //Error + qDebug() << mDb.error_msg(); + return false; + } + + emit stationAdded(stationId); + + if(outStationId) + *outStationId = stationId; + + return true; +} + +bool LineStorage::removeStation(db_id stationId) +{ + command q_removeStation(mDb, "DELETE FROM stations WHERE id=?"); + + q_removeStation.bind(1, stationId); + int ret = q_removeStation.execute(); + q_removeStation.reset(); + + if(ret != SQLITE_OK) + { + qDebug() << Q_FUNC_INFO << "Error:" << ret << mDb.error_msg(); + int err = mDb.extended_error_code(); + qDebug() << "Extended error:" << err; + if(err == SQLITE_CONSTRAINT_TRIGGER) + { + qDebug() << "Station" << stationId << "IS IN USE"; + + //emit modelError(tr(errorStationInUse).arg(iter.value().name)); + } + return false; + } + + emit stationRemoved(stationId); + + auto st = impl->stations.find(stationId); + if(st == impl->stations.end()) + return true; + + //Delete station graph + Q_ASSERT_X(st.value().lineGraphs.isEmpty(), "LineStorage", "Deleted station that was still in at least one line"); + impl->stations.erase(st); + + return true; +} + +//You must manually redrawLine(lineId) after adding a station +void LineStorage::addStationToLine(db_id lineId, db_id stId) +{ + auto it = impl->lines.find(lineId); + if(it == impl->lines.end()) + return; + + LineObj &obj = it.value(); + impl->addStation(obj, stId); +} + +//You must manually redrawLine(lineId) after removing a station +void LineStorage::removeStationFromLine(db_id lineId, db_id stId) +{ + auto it = impl->lines.find(lineId); + if(it == impl->lines.end()) + return; + + LineObj &obj = it.value(); + impl->removeStation(obj, stId); +} + +bool LineStorage::releaseLine(db_id lineId) +{ + auto it = impl->lines.find(lineId); + if(it == impl->lines.end()) + return false; + LineObj &line = it.value(); + line.refCount--; + if(line.refCount <= 0) + { + //Unload line FIXME: schedule deferred unsload, 5 seconds + impl->unloadLine(it); + } + return true; +} + +bool LineStorage::increfLine(db_id lineId) +{ + auto it = impl->lines.find(lineId); + if(it == impl->lines.end()) + { + //Load line, check if exists + query q(mDb, "SELECT id FROM lines WHERE id=?"); + q.bind(1, lineId); + if(q.step() != SQLITE_ROW) + return false; + + LineObj obj; + obj.lineId = lineId; + + it = impl->lines.insert(obj.lineId, obj); + impl->loadLine(it.value()); + impl->drawStations(it.value()); + impl->drawJobs(it->lineId); + } + + LineObj &line = it.value(); + line.refCount++; + return true; +} + +QGraphicsScene *LineStorage::sceneForLine(db_id lineId) +{ + auto it = impl->lines.constFind(lineId); + if(it == impl->lines.constEnd()) + return nullptr; + return it.value().scene; +} + +void LineStorage::redrawAllLines() +{ + for(LineObj &obj : impl->lines) + { + impl->drawStations(obj); + emit lineStationsModified(obj.lineId); + } +} + +void LineStorage::redrawLine(db_id lineId) +{ + auto it = impl->lines.find(lineId); + if(it == impl->lines.end()) + return; + impl->drawStations(it.value()); + impl->drawJobs(lineId); + emit lineStationsModified(lineId); +} + +void LineStorage::addJobStop(db_id stopId, db_id stId, db_id jobId, db_id lineId, const QString &label, qreal y1, qreal y2, int platf) +{ + //FIXME: check + auto station = impl->stations.find(stId); + if(station == impl->stations.end()) + return; + station->addJobStop(stopId, jobId, lineId, label, y1, y2, platf); +} + +//Removes all stop shadows in other lines +void LineStorage::removeJobStops(db_id jobId) +{ + for(StationObj &obj : impl->stations) + { + obj.removeJob(jobId); + } +} + +void LineStorage::updateStationColor(db_id stationId) +{ + if(!mDb.db()) + return; + + auto st = impl->stations.find(stationId); + if(st == impl->stations.end()) + return; + + query q(mDb, "SELECT platf_color FROM stations WHERE id=?"); + q.bind(1, stationId); + if(q.step() != SQLITE_ROW) + return; //Error! + QRgb color = QRgb(q.getRows().get(0)); + st->setPlatfColor(color); +} + +void LineStorage::onStationModified(db_id stationId) +{ + if(!mDb.db()) + return; + + auto st = impl->stations.find(stationId); + if(st == impl->stations.end()) + return; + + query q(mDb, "SELECT platforms,depot_platf FROM stations WHERE id=?"); + q.bind(1, stationId); + if(q.step() != SQLITE_ROW) + return; //Error! + auto r = q.getRows(); + st->platfs = r.get(0); + st->depots = r.get(1); + + //Update all line this station is in and jobs that pass for this line. + //Previously we did redrawAllLines() but that's a waste of time + //And without pessimistic lock, instead of batching changes we get + //'stationModified()' for every single change + for(const auto &it : qAsConst(st->lineGraphs)) + { + auto line = impl->lines.find(it->lineId); + Q_ASSERT_X(line != impl->lines.end(), "LineStorage", "Station has a ghost line"); + + impl->drawStations(line.value()); + impl->drawJobs(it->lineId); + + emit lineStationsModified(it->lineId); + } +} diff --git a/src/lines/linestorage.h b/src/lines/linestorage.h new file mode 100644 index 0000000..8b0715b --- /dev/null +++ b/src/lines/linestorage.h @@ -0,0 +1,68 @@ +#ifndef LINESTORAGE_H +#define LINESTORAGE_H + +#include + +#include "utils/types.h" + +namespace sqlite3pp { +class database; +} + +class LineStoragePrivate; +class QGraphicsScene; + +class LineStorage : public QObject +{ + Q_OBJECT +public: + LineStorage(sqlite3pp::database &db, QObject *parent = nullptr); + ~LineStorage(); + + void clear(); + + bool addLine(db_id *outLineId = nullptr); + bool removeLine(db_id lineId); + + bool addStation(db_id *outStationId = nullptr); + bool removeStation(db_id stationId); + + void addStationToLine(db_id lineId, db_id stId); + void removeStationFromLine(db_id lineId, db_id stId); + + bool releaseLine(db_id lineId); + bool increfLine(db_id lineId); + QGraphicsScene *sceneForLine(db_id lineId); + + void redrawAllLines(); + void redrawLine(db_id lineId); + + void addJobStop(db_id stopId, db_id stId, db_id jobId, db_id lineId, + const QString &label, qreal y1, qreal y2, int platf); + void removeJobStops(db_id jobId); + + +signals: + void lineAdded(db_id lineId); + void lineNameChanged(db_id lineId); + void lineStationsModified(db_id lineId); + void lineAboutToBeRemoved(db_id lineId); + void lineRemoved(db_id lineId); + + void stationAdded(db_id stationId); + void stationNameChanged(db_id stationId); + void stationModified(db_id stationId); + void stationColorChanged(db_id stationId); + void stationPlanChanged(db_id stationId); + void stationRemoved(db_id stationId); + +private slots: + void onStationModified(db_id stId); + void updateStationColor(db_id stationId); + +private: + sqlite3pp::database &mDb; + LineStoragePrivate *impl; +}; + +#endif // LINESTORAGE_H diff --git a/src/odt_export/CMakeLists.txt b/src/odt_export/CMakeLists.txt new file mode 100644 index 0000000..0326a35 --- /dev/null +++ b/src/odt_export/CMakeLists.txt @@ -0,0 +1,15 @@ +add_subdirectory(common) + +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + odt_export/jobsheetexport.h + odt_export/sessionrsexport.h + odt_export/shiftsheetexport.h + odt_export/stationsheetexport.h + + odt_export/jobsheetexport.cpp + odt_export/sessionrsexport.cpp + odt_export/shiftsheetexport.cpp + odt_export/stationsheetexport.cpp + PARENT_SCOPE +) diff --git a/src/odt_export/common/CMakeLists.txt b/src/odt_export/common/CMakeLists.txt new file mode 100644 index 0000000..1638151 --- /dev/null +++ b/src/odt_export/common/CMakeLists.txt @@ -0,0 +1,15 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + odt_export/common/sessionrswriter.h + odt_export/common/jobwriter.h + odt_export/common/odtdocument.h + odt_export/common/odtutils.h + odt_export/common/stationwriter.h + + odt_export/common/sessionrswriter.cpp + odt_export/common/jobwriter.cpp + odt_export/common/odtdocument.cpp + odt_export/common/odtutils.cpp + odt_export/common/stationwriter.cpp + PARENT_SCOPE +) diff --git a/src/odt_export/common/jobwriter.cpp b/src/odt_export/common/jobwriter.cpp new file mode 100644 index 0000000..676e7aa --- /dev/null +++ b/src/odt_export/common/jobwriter.cpp @@ -0,0 +1,910 @@ +#include "jobwriter.h" + +#include "odtutils.h" +#include + +#include "utils/platform_utils.h" +#include "app/session.h" + +#include "utils/rs_utils.h" +#include "utils/jobcategorystrings.h" + +#include + + +void writeJobSummary(QXmlStreamWriter& xml, + const QString& from, const QString& dep, + const QString& to, const QString& arr, + int axes) +{ + //Table 'job_summary' + xml.writeStartElement("table:table"); + xml.writeAttribute("table:name", "job_summary"); + xml.writeAttribute("table:style-name", "job_5f_summary"); + + xml.writeEmptyElement("table:table-column"); //A + xml.writeAttribute("table:style-name", "job_5f_summary.A"); + + xml.writeEmptyElement("table:table-column"); //B + xml.writeAttribute("table:style-name", "job_5f_summary.B"); + + xml.writeEmptyElement("table:table-column"); //C + xml.writeAttribute("table:style-name", "job_5f_summary.C"); + + xml.writeEmptyElement("table:table-column"); //D + xml.writeAttribute("table:style-name", "job_5f_summary.D"); + + //Row + xml.writeStartElement("table:table-row"); + + //Cells + writeCell(xml, "job_5f_summary_cell", "P2", Odt::tr("From:")); + writeCell(xml, "job_5f_summary_cell", "P3", from); + writeCell(xml, "job_5f_summary_cell", "P2", Odt::tr("Departure:")); + writeCell(xml, "job_5f_summary_cell", "P3", dep); + + xml.writeEndElement(); //table-row + + //Row 2 + xml.writeStartElement("table:table-row"); + + //Cells + writeCell(xml, "job_5f_summary_cell", "P2", Odt::tr("To:")); + writeCell(xml, "job_5f_summary_cell", "P3", to); + writeCell(xml, "job_5f_summary_cell", "P2", Odt::tr("Arrival:")); + writeCell(xml, "job_5f_summary_cell", "P3", arr); + + xml.writeEndElement(); //table-row + + //Row 3 + xml.writeStartElement("table:table-row"); + + //Cells + writeCell(xml, "job_5f_summary_cell", "P2", Odt::tr("Axes:")); + writeCell(xml, "job_5f_summary_cell", "P3", QString::number(axes)); + writeCell(xml, "job_5f_summary_cell", "P2", QString()); + writeCell(xml, "job_5f_summary_cell", "P3", QString()); + + xml.writeEndElement(); //table-row + + xml.writeEndElement(); //table:table END +} + + +JobWriter::JobWriter(database &db) : + mDb(db), + q_getJobStops(mDb, "SELECT stops.id," + "stops.stationId," + "stations.name," + "stops.arrival," + "stops.departure," + "stops.platform," + "stops.transit," + "stops.description " + "FROM stops " + "JOIN stations ON stations.id=stops.stationId " + "WHERE stops.jobId=? ORDER BY stops.arrival"), + + q_getFirstLastStops(mDb, "SELECT s1.id," + " st1.name," + " s1.departure," + " s2.id," + " st2.name," + " s2.arrival" + " FROM jobs" + " JOIN stops s1 ON s1.id=jobs.firstStop" + " JOIN stops s2 ON s2.id=jobs.lastStop" + " JOIN stations st1 ON st1.id=s1.stationId" + " JOIN stations st2 ON st2.id=s2.stationId" + " WHERE jobs.id=?"), + + q_initialJobAxes(mDb, "SELECT SUM(rs_models.axes)" + " FROM coupling" + " JOIN rs_list ON rs_list.id=coupling.rsId" + " JOIN rs_models ON rs_models.id=rs_list.model_id" + " WHERE stopId=?"), + q_selectPassings(mDb, "SELECT stops.id,stops.jobId,jobs.category,stops.arrival,stops.departure,stops.platform" + " FROM stops" + " JOIN jobs ON jobs.id=stops.jobId" + " WHERE stops.stationId=? AND stops.departure>=? AND stops.arrival<=? AND stops.jobId<>?"), + q_getStopCouplings(mDb, "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" + " JOIN rs_models ON rs_models.id=rs_list.model_id" + " WHERE coupling.stopId=? AND coupling.operation=?") +{ + +} + +void JobWriter::writeJobAutomaticStyles(QXmlStreamWriter &xml) +{ + //job_summary columns + writeColumnStyle(xml, "job_5f_summary.A", "1.60cm"); + writeColumnStyle(xml, "job_5f_summary.B", "8.30cm"); + writeColumnStyle(xml, "job_5f_summary.C", "2.90cm"); + writeColumnStyle(xml, "job_5f_summary.D", "4.20cm"); + + //job_stops columns + writeColumnStyle(xml, "job_5f_stops.A", "2.60cm"); //Station (IT: Stazione) + writeColumnStyle(xml, "job_5f_stops.B", "1.60cm"); //Arrival (IT: Arrivo) + writeColumnStyle(xml, "job_5f_stops.C", "2.10cm"); //Departure (IT: Partenza) + writeColumnStyle(xml, "job_5f_stops.D", "1.cm"); //Platorm 'Platf' (IT: Binario 'Bin') + writeColumnStyle(xml, "job_5f_stops.E", "3.00cm"); //Rollingstock (IT: Rotabili) + writeColumnStyle(xml, "job_5f_stops.F", "2.30cm"); //Crossings + writeColumnStyle(xml, "job_5f_stops.G", "2.30cm"); //Passings + writeColumnStyle(xml, "job_5f_stops.H", "3.20cm"); //Description (IT: Note) + + /* Style: job_5f_stops.A1 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on left, top, bottom sides + * Padding: 0.030cm all sides except bottom + * padding-bottom: 0.15cm + * + * Usage: + * - job_5f_stops table: top left/middle cells (except top right which has H1 style) + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "job_5f_stops.A1"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding-left", "0.030cm"); + xml.writeAttribute("fo:padding-right", "0.030cm"); + xml.writeAttribute("fo:padding-top", "0.030cm"); + xml.writeAttribute("fo:padding-bottom", "0.15cm"); + xml.writeAttribute("fo:border-left", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-right", "none"); + xml.writeAttribute("fo:border-top", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-bottom", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + /* Style: job_5f_stops.H1 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on all sides + * Padding: 0.030cm all sides except bottom + * padding-bottom: 0.15cm + * + * Usage: + * - job_5f_stops table: top right cell + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "job_5f_stops.H1"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding-left", "0.030cm"); + xml.writeAttribute("fo:padding-right", "0.030cm"); + xml.writeAttribute("fo:padding-top", "0.030cm"); + xml.writeAttribute("fo:padding-bottom", "0.15cm"); + xml.writeAttribute("fo:border", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + /* Style: job_5f_stops.A2 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on left and bottom sides + * Padding: 0.049cm all sides + * + * Usage: + * - job_5f_stops table: right and middle cells from second row to last row + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "job_5f_stops.A2"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding", "0.049cm"); + xml.writeAttribute("fo:border-left", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-right", "none"); + xml.writeAttribute("fo:border-top", "none"); + xml.writeAttribute("fo:border-bottom", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + /* Style: job_5f_stops.H2 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on left, right and bottom sides + * Padding: 0.049cm all sides + * + * Usage: + * - job_5f_stops table: left cells from second row to last row + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "job_5f_stops.H2"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding", "0.049cm"); + xml.writeAttribute("fo:border-left", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-right", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-top", "none"); + xml.writeAttribute("fo:border-bottom", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + //job_5f_asset columns + writeColumnStyle(xml, "job_5f_asset.A", "3.0cm"); + writeColumnStyle(xml, "job_5f_asset.B", "14.0cm"); + + /* Style: job_5f_asset.A1 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on left, top, bottom sides + * Padding: 0.049cm + * + * Usage: + * - job_asset table: top left cell + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "job_5f_asset.A1"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding", "0.049cm"); + xml.writeAttribute("fo:border-left", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-right", "none"); + xml.writeAttribute("fo:border-top", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-bottom", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + /* Style: job_5f_asset.B1 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on all sides + * Padding: 0.049cm + * + * Usage: + * - job_asset table: top right cell + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "job_5f_asset.B1"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding", "0.049cm"); + xml.writeAttribute("fo:border", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + /* Style: job_5f_asset.A2 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on right and bottom sides + * Padding: 0.049cm + * + * Usage: + * - job_asset table: bottom left cell + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "job_5f_asset.A2"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding", "0.049cm"); + xml.writeAttribute("fo:border-left", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-right", "none"); + xml.writeAttribute("fo:border-top", "none"); + xml.writeAttribute("fo:border-bottom", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + /* Style: job_5f_asset.B2 + * + * Type: table-cell + * Border: 0.05pt solid #000000 all sides except top + * Padding: 0.049cm + * + * Usage: + * - job_asset table: bottom left cell + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "job_5f_asset.B2"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding", "0.049cm"); + xml.writeAttribute("fo:border-left", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-right", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-top", "none"); + xml.writeAttribute("fo:border-bottom", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style +} + +void JobWriter::writeJobStyles(QXmlStreamWriter& xml) +{ + /* Style: job_5f_summary + * + * Type: table + * Display name: job_summary + * Align: left + * Width: 8.0cm + * + * Usage: + * - job_summary table: displays summary information about the job + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table"); + xml.writeAttribute("style:name", "job_5f_summary"); + xml.writeAttribute("style:display-name", "job_summary"); + xml.writeStartElement("style:table-properties"); + xml.writeAttribute("style:shadow", "none"); + xml.writeAttribute("table:align", "left"); + xml.writeAttribute("style:width", "8.0cm"); + xml.writeEndElement(); //style:table-properties + xml.writeEndElement(); //style + + /* Style: job_5f_summary_cell + * + * Type: table-cell + * Border: none + * Padding: 0.097cm + * + * Usage: + * - job_summary table: do not show borders so we fake text layout in a invisible table grid + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "job_5f_summary_cell"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:border", "none"); + xml.writeAttribute("fo:padding", "0.097cm"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + /* Style: job_5f_stops + * + * Type: table + * Display name: job_stops + * Align: left + * Width: 16.0cm + * + * Usage: + * - job_stops table: displays job stops + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table"); + xml.writeAttribute("style:name", "job_5f_stops"); + xml.writeAttribute("style:display-name", "job_stops"); + xml.writeStartElement("style:table-properties"); + xml.writeAttribute("table:align", "left"); + xml.writeAttribute("style:width", "16.0cm"); + + xml.writeEndElement(); //style:table-properties + xml.writeEndElement(); //style + + /* Style: job_5f_asset + * + * Type: table + * Display name: job_asset + * Align: left + * Width: 16.0cm + * + * Usage: + * - job_stops table: displays job rollingstock asset summary + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table"); + xml.writeAttribute("style:name", "job_5f_asset"); + xml.writeAttribute("style:display-name", "job_asset"); + xml.writeStartElement("style:table-properties"); + xml.writeAttribute("table:align", "left"); + xml.writeAttribute("style:width", "16.0cm"); + + xml.writeEndElement(); //style:table-properties + xml.writeEndElement(); //style + + /* Style P2 + * type: paragraph + * text-align: start + * font-size: 16pt + * font-weight: bold + * + * Usages: + * - job_summary: summary title fields + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:name", "P2"); + + xml.writeStartElement("style:paragraph-properties"); + xml.writeAttribute("fo:text-align", "start"); + xml.writeAttribute("style:justify-single-word", "false"); + xml.writeEndElement(); //style:paragraph-properties + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("fo:font-size", "16pt"); + xml.writeAttribute("fo:font-weight", "bold"); + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style + + /* Style P3 + * type: paragraph + * text-align: start + * font-size: 16pt + * + * Description + * Like P2 but not bold + * + * Usages: + * - job_summary: summary value fields + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:name", "P3"); + + xml.writeStartElement("style:paragraph-properties"); + xml.writeAttribute("fo:text-align", "start"); + xml.writeAttribute("style:justify-single-word", "false"); + xml.writeEndElement(); //style:paragraph-properties + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("fo:font-size", "16pt"); + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style + + /* Style P5 + * type: paragraph + * text-align: center + * font-size: 12pt + * + * Description: + * Like P4 but not bold + * + * Usages: + * - job_stops: stop cell text for normal stops and transit Rollingstock/Crossings/Passings/Description + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:name", "P5"); + + xml.writeStartElement("style:paragraph-properties"); + xml.writeAttribute("fo:text-align", "center"); + xml.writeAttribute("style:justify-single-word", "false"); + xml.writeEndElement(); //style:paragraph-properties + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("fo:font-size", "12pt"); + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style + + /* Style P6 + * type: paragraph + * text-align: center + * font-size: 12pt + * font-style: italic + * + * Description: + * Like P5 but Italic + * (P4 + Italic, not bold) + * + * Usages: + * - job_stops: stop cell text for transit stops except for Rollingstock/Crossings/Passings/Description columns which have P5 + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:name", "P6"); + + xml.writeStartElement("style:paragraph-properties"); + xml.writeAttribute("fo:text-align", "center"); + xml.writeAttribute("style:justify-single-word", "false"); + xml.writeEndElement(); //style:paragraph-properties + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("fo:font-size", "12pt"); + xml.writeAttribute("fo:font-style", "italic"); + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style + + + //stile interruzione di pagina + //TODO: quando useremo 'Page master style' vedere se vanno in conflitto + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:name", "interruzione"); + + xml.writeStartElement("style:paragraph-properties"); + xml.writeAttribute("fo:text-align", "start"); + xml.writeAttribute("fo:break-after", "page"); + xml.writeEndElement(); //style:paragraph-properties + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("fo:font-size", "1pt"); + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style +} + +void JobWriter::writeJob(QXmlStreamWriter& xml, db_id jobId, JobCategory jobCat) +{ + query q_getRSInfo(mDb, "SELECT rs_list.number,rs_models.name,rs_models.suffix,rs_models.type" + " FROM rs_list" + " LEFT JOIN rs_models ON rs_models.id=rs_list.model_id" + " WHERE rs_list.id=?"); + + QList>> stopsRS; + + //Title + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P1"); + xml.writeCharacters(JobCategoryName::jobName(jobId, jobCat)); + xml.writeEndElement(); + + //Vertical space + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P1"); + xml.writeEndElement(); + + db_id firstStopId = 0; + db_id lastStopId = 0; + + //Job summary + q_getFirstLastStops.bind(1, jobId); + if(q_getFirstLastStops.step() == SQLITE_ROW) + { + auto r = q_getFirstLastStops.getRows(); + + firstStopId = r.get(0); + QString from = r.get(1); + QTime dep = r.get(2); + lastStopId = r.get(3); + QString to = r.get(4); + QTime arr = r.get(5); + + q_initialJobAxes.bind(1, firstStopId); + q_initialJobAxes.step(); + int axes = q_initialJobAxes.getRows().get(0); + q_initialJobAxes.reset(); + + writeJobSummary(xml, + from, dep.toString("HH:mm"), + to, arr.toString("HH:mm"), + axes); + } + else + { + qDebug() << "Error: getting first/last stations names FAILED\n" + << mDb.error_code() << mDb.error_msg(); + writeJobSummary(xml, "err", "err", "err", "err", 0); + } + q_getFirstLastStops.reset(); + + //Vertical space + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P1"); + xml.writeEndElement(); + + //Table 'job_stops' + xml.writeStartElement("table:table"); + xml.writeAttribute("table:name", "job_stops"); + xml.writeAttribute("table:style-name", "job_5f_stops"); + + xml.writeEmptyElement("table:table-column"); //Station + xml.writeAttribute("table:style-name", "job_5f_stops.A"); + + xml.writeEmptyElement("table:table-column"); //Arrival + xml.writeAttribute("table:style-name", "job_5f_stops.B"); + + xml.writeEmptyElement("table:table-column"); //Departure + xml.writeAttribute("table:style-name", "job_5f_stops.C"); + + xml.writeEmptyElement("table:table-column"); //Platform (Platf) + xml.writeAttribute("table:style-name", "job_5f_stops.D"); + + xml.writeEmptyElement("table:table-column"); //Rollingstock + xml.writeAttribute("table:style-name", "job_5f_stops.E"); + + xml.writeEmptyElement("table:table-column"); //Crossings + xml.writeAttribute("table:style-name", "job_5f_stops.F"); + + xml.writeEmptyElement("table:table-column"); //Passings + xml.writeAttribute("table:style-name", "job_5f_stops.G"); + + xml.writeEmptyElement("table:table-column"); //Description + xml.writeAttribute("table:style-name", "job_5f_stops.H"); + + //Row 1 (Heading) + xml.writeStartElement("table:table-header-rows"); + xml.writeStartElement("table:table-row"); + + const QString P4_Style = "P4"; + writeCell(xml, "job_5f_stops.A1", P4_Style, Odt::tr("Station")); + writeCell(xml, "job_5f_stops.A1", P4_Style, Odt::tr("Arrival")); + writeCell(xml, "job_5f_stops.A1", P4_Style, Odt::tr("Departure")); + writeCell(xml, "job_5f_stops.A1", P4_Style, Odt::tr("Platf")); + writeCell(xml, "job_5f_stops.A1", P4_Style, Odt::tr("Rollingstock")); + writeCell(xml, "job_5f_stops.A1", P4_Style, Odt::tr("Crossings")); + writeCell(xml, "job_5f_stops.A1", P4_Style, Odt::tr("Passings")); + writeCell(xml, "job_5f_stops.H1", P4_Style, Odt::tr("Notes")); //Description + + xml.writeEndElement(); //end of row + xml.writeEndElement(); //header section + + QList rsAsset; + + const QString P5_style = "P5"; + + //Fill stops table + q_getJobStops.bind(1, jobId); + for(auto stop : q_getJobStops) + { + db_id stopId = stop.get(0); + db_id stationId = stop.get(1); + QString stationName = stop.get(2); + QTime arr = stop.get(3); + QTime dep = stop.get(4); + int platf = stop.get(5); + int isTransit = stop.get(6); + QString descr = stop.get(7); + + qDebug() << "(Loop) Job:" << jobId << "Stop:" << stopId; + + xml.writeStartElement("table:table-row"); //start new row + + const QString styleName = isTransit ? "P6" : P5_style; //If it's transit use italic style + + //Station + writeCell(xml, "job_5f_stops.A2", styleName, stationName); + + //Arrival + writeCell(xml, "job_5f_stops.A2", styleName, stopId == firstStopId ? QString() : arr.toString("HH:mm")); + + //Departure + //If it's transit then and arrival is equal to departure (should be always but if is different show both to warn user about the error) + //then show only arrival + writeCell(xml, "job_5f_stops.A2", styleName, (stopId == lastStopId || (isTransit && arr == dep)) ? QString() : dep.toString("HH:mm")); + + //Platform + writeCell(xml, "job_5f_stops.A2", styleName, utils::platformName(platf)); + + //Rollingstock + sqlite3_stmt *stmt = q_getStopCouplings.stmt(); + writeCellListStart(xml, "job_5f_stops.A2", P5_style); + + //Coupled rollingstock + bool firstCoupRow = true; + q_getStopCouplings.bind(1, stopId); + q_getStopCouplings.bind(2, RsOp::Coupled); + for(auto coup : q_getStopCouplings) + { + db_id rsId = coup.get(0); + rsAsset.append(rsId); + + int number = coup.get(1); + int modelNameLen = sqlite3_column_bytes(stmt, 2); + const char *modelName = reinterpret_cast(sqlite3_column_text(stmt, 2)); + + int modelSuffixLen = sqlite3_column_bytes(stmt, 3); + const char *modelSuffix = reinterpret_cast(sqlite3_column_text(stmt, 3)); + RsType type = RsType(sqlite3_column_int(stmt, 4)); + + const QString rsName = rs_utils::formatNameRef(modelName, modelNameLen, + number, + modelSuffix, modelSuffixLen, + type); + + if(firstCoupRow) + { + firstCoupRow = false; + //Use bold font + xml.writeStartElement("text:span"); + xml.writeAttribute("text:style-name", "T1"); + xml.writeCharacters(Odt::tr(Odt::CoupledAbbr)); + xml.writeEndElement(); //test:span + } + + xml.writeEmptyElement("text:line-break"); + xml.writeCharacters(rsName); + } + q_getStopCouplings.reset(); + + //Unoupled rollingstock + bool firstUncoupRow = true; + q_getStopCouplings.bind(1, stopId); + q_getStopCouplings.bind(2, RsOp::Uncoupled); + for(auto coup : q_getStopCouplings) + { + db_id rsId = coup.get(0); + rsAsset.removeAll(rsId); + + int number = coup.get(1); + int modelNameLen = sqlite3_column_bytes(stmt, 2); + const char *modelName = reinterpret_cast(sqlite3_column_text(stmt, 2)); + + int modelSuffixLen = sqlite3_column_bytes(stmt, 3); + const char *modelSuffix = reinterpret_cast(sqlite3_column_text(stmt, 3)); + RsType type = RsType(sqlite3_column_int(stmt, 4)); + + const QString rsName = rs_utils::formatNameRef(modelName, modelNameLen, + number, + modelSuffix, modelSuffixLen, + type); + + if(firstUncoupRow) + { + if(!firstCoupRow) //Not first row, there were coupled rs + xml.writeEmptyElement("text:line-break"); //Separate from coupled + firstUncoupRow = false; + //Use bold font + xml.writeStartElement("text:span"); + xml.writeAttribute("text:style-name", "T1"); + xml.writeCharacters(Odt::tr(Odt::UncoupledAbbr)); + xml.writeEndElement(); //test:span + } + + xml.writeEmptyElement("text:line-break"); + xml.writeCharacters(rsName); + } + q_getStopCouplings.reset(); + writeCellListEnd(xml); + + stopsRS.append({stationName, rsAsset}); + + //Crossings / Passings + Direction myDir = Session->getStopDirection(stopId, stationId); + + QVector passings; + + q_selectPassings.bind(1, stationId); + q_selectPassings.bind(2, arr); + q_selectPassings.bind(3, dep); + q_selectPassings.bind(4, jobId); + + //Incroci + firstCoupRow = true; + writeCellListStart(xml, "job_5f_stops.A2", P5_style); + for(auto pass : q_selectPassings) + { + db_id otherStopId = pass.get(0); + db_id otherJobId = pass.get(1); + JobCategory otherJobCat = JobCategory(pass.get(2)); + + //QTime otherArr = pass.get(3); + //QTime otherDep = pass.get(4); + + Direction otherDir = Session->getStopDirection(otherStopId, stationId); + + if(myDir == otherDir) + passings.append({otherJobId, otherJobCat}); + else + { + if(firstCoupRow) + firstCoupRow = false; + else + xml.writeEmptyElement("text:line-break"); + xml.writeCharacters(JobCategoryName::jobName(otherJobId, otherJobCat)); + } + } + q_selectPassings.reset(); + writeCellListEnd(xml); + + //Passings + firstCoupRow = true; + writeCellListStart(xml, "job_5f_stops.A2", P5_style); + for(auto entry : passings) + { + if(firstCoupRow) + firstCoupRow = false; + else + xml.writeEmptyElement("text:line-break"); + xml.writeCharacters(JobCategoryName::jobName(entry.jobId, entry.category)); + } + writeCellListEnd(xml); + + //Description + writeCellListStart(xml, "job_5f_stops.H2", P5_style); + if(!descr.isEmpty()) + { + //Split in lines + int lastIdx = 0; + while(true) + { + int idx = descr.indexOf('\n', lastIdx); + QString line = descr.mid(lastIdx, idx == -1 ? idx : idx - lastIdx); + xml.writeCharacters(line.simplified()); + if(idx < 0) + break; //Last line + lastIdx = idx + 1; + xml.writeEmptyElement("text:line-break"); + } + } + writeCellListEnd(xml); + + xml.writeEndElement(); //end of row + } + q_getJobStops.reset(); + + xml.writeEndElement(); //table:table END + + //text:p as separator + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P1"); + xml.writeEndElement(); + + //Table 'job_asset' + xml.writeStartElement("table:table"); + xml.writeAttribute("table:name", "job_asset"); + xml.writeAttribute("table:style-name", "job_5f_asset"); + + xml.writeEmptyElement("table:table-column"); //Stazione + xml.writeAttribute("table:style-name", "job_5f_asset.A"); + + xml.writeEmptyElement("table:table-column"); //Assetto + xml.writeAttribute("table:style-name", "job_5f_asset.B"); + + //Duplicate second-last asset to last stop because last stop would be always empty + if(stopsRS.size() >= 2) + { + int i = stopsRS.size() - 2; //Get second-last (IT: penultima fermata) + stopsRS[i + 1].second = stopsRS[i].second; + } + else + { + //Error! + qWarning() << __FUNCTION__ << "At least 2 stops required!"; + } + + bool firstRow = true; + for(auto &s : qAsConst(stopsRS)) + { + xml.writeStartElement("table:table-row"); //start new row + + writeCell(xml, firstRow ? "job_5f_asset.A1" : "job_5f_asset.A2", P5_style, s.first); + + writeCellListStart(xml, firstRow ? "job_5f_asset.B1" : "job_5f_asset.B2", P5_style); + for(int i = 0; i < s.second.size(); i++) + { + q_getRSInfo.reset(); + q_getRSInfo.bind(1, s.second.at(i)); + int ret = q_getRSInfo.step(); + if(ret != SQLITE_ROW) + { + //Error: RS does not exist! + continue; + } + + sqlite3_stmt *stmt = q_getRSInfo.stmt(); + int number = sqlite3_column_int(stmt, 0); + int modelNameLen = sqlite3_column_bytes(stmt, 1); + const char *modelName = reinterpret_cast(sqlite3_column_text(stmt, 1)); + + int modelSuffixLen = sqlite3_column_bytes(stmt, 2); + const char *modelSuffix = reinterpret_cast(sqlite3_column_text(stmt, 2)); + RsType type = RsType(sqlite3_column_int(stmt, 3)); + + const QString name = rs_utils::formatNameRef(modelName, modelNameLen, + number, + modelSuffix, modelSuffixLen, + type); + + xml.writeCharacters(name); + if(i < s.second.size() - 1) + xml.writeCharacters(" + "); + } + writeCellListEnd(xml); + + xml.writeEndElement(); //end of row + + if(firstRow) + firstRow = false; + } + + xml.writeEndElement(); + + //Interruzione pagina TODO: see style 'interruzione' + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "interruzione"); + xml.writeEndElement(); +} diff --git a/src/odt_export/common/jobwriter.h b/src/odt_export/common/jobwriter.h new file mode 100644 index 0000000..9a7d121 --- /dev/null +++ b/src/odt_export/common/jobwriter.h @@ -0,0 +1,31 @@ +#ifndef JOBWRITER_H +#define JOBWRITER_H + +#include "utils/types.h" + +#include +using namespace sqlite3pp; + +class QXmlStreamWriter; + +class JobWriter +{ +public: + JobWriter(database& db); + + static void writeJobAutomaticStyles(QXmlStreamWriter& xml); + static void writeJobStyles(QXmlStreamWriter &xml); + + void writeJob(QXmlStreamWriter &xml, db_id jobId, JobCategory jobCat); + +private: + database &mDb; + + query q_getJobStops; + query q_getFirstLastStops; + query q_initialJobAxes; + query q_selectPassings; + query q_getStopCouplings; +}; + +#endif // JOBWRITER_H diff --git a/src/odt_export/common/odtdocument.cpp b/src/odt_export/common/odtdocument.cpp new file mode 100644 index 0000000..2e7c643 --- /dev/null +++ b/src/odt_export/common/odtdocument.cpp @@ -0,0 +1,420 @@ +#include "odtdocument.h" + +#include + +#include + +#include + +#include "info.h" //Fot App constants +#include "app/session.h" //For settings +#include "db_metadata/metadatamanager.h" + +#include "odtutils.h" + +//content.xml +static constexpr char contentFileStr[] = "content.xml"; +static constexpr QLatin1String contentFileName = QLatin1String(contentFileStr, sizeof (contentFileStr) - 1); + +//styles.xml +static constexpr char stylesFileStr[] = "styles.xml"; +static constexpr QLatin1String stylesFileName = QLatin1String(stylesFileStr, sizeof (stylesFileStr) - 1); + +//meta.xml +static constexpr char metaFileStr[] = "meta.xml"; +static constexpr QLatin1String metaFileName = QLatin1String(metaFileStr, sizeof (metaFileStr) - 1); + +//META-INF/manifest.xml +static constexpr char manifestFileNameStr[] = "manifest.xml"; +static constexpr QLatin1String manifestFileName = QLatin1String(manifestFileNameStr, sizeof (manifestFileNameStr) - 1); +static constexpr char metaInfPathStr[] = "/META-INF"; +static constexpr QLatin1String metaInfDirPath = QLatin1String(metaInfPathStr, sizeof (metaInfPathStr) - 1); +static constexpr char manifestFilePathStr[] = "META-INF/manifest.xml"; +static constexpr QLatin1String manifestFilePath = QLatin1String(manifestFilePathStr, sizeof (manifestFilePathStr) - 1); + +OdtDocument::OdtDocument() +{ + +} + +bool OdtDocument::initDocument() +{ + if(!dir.isValid()) + return false; + + content.setFileName(dir.filePath(contentFileName)); + if(!content.open(QFile::WriteOnly | QFile::Truncate)) + return false; + + styles.setFileName(dir.filePath(stylesFileName)); + if(!styles.open(QFile::WriteOnly | QFile::Truncate)) + return false; + + contentXml.setDevice(&content); + stylesXml.setDevice(&styles); + + //Init content.xml + writeStartDoc(contentXml); + contentXml.writeStartElement("office:document-content"); + contentXml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:office:1.0", "office"); + contentXml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:style:1.0", "style"); + contentXml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:text:1.0", "text"); + contentXml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:table:1.0", "table"); + contentXml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", "fo"); + contentXml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0", "svg"); + contentXml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:drawing:1.0", "draw"); + contentXml.writeNamespace("http://www.w3.org/1999/xlink", "xlink"); + contentXml.writeAttribute("office:version", "1.2"); + + //Init styles.xml + writeStartDoc(stylesXml); + stylesXml.writeStartElement("office:document-styles"); + stylesXml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:office:1.0", "office"); + stylesXml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:style:1.0", "style"); + stylesXml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:text:1.0", "text"); + stylesXml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:table:1.0", "table"); + stylesXml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", "fo"); + stylesXml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0", "svg"); + stylesXml.writeNamespace("http://www.w3.org/1999/xlink", "xlink"); + stylesXml.writeAttribute("office:version", "1.2"); + + return true; +} + +void OdtDocument::startBody() //TODO: start body manually, remove this function +{ + contentXml.writeEndElement(); //office:automatic-styles + + contentXml.writeStartElement("office:body"); + contentXml.writeStartElement("office:text"); +} + +bool OdtDocument::saveTo(const QString& fileName) +{ + int err = 0; + zip_t *zipper = zip_open(fileName.toUtf8(), ZIP_CREATE | ZIP_TRUNCATE, &err); + + if (zipper == nullptr) + { + zip_error_t ziperror; + zip_error_init_with_code(&ziperror, err); + qDebug() << "Failed to open output file" << fileName << "Err:" << zip_error_strerror(&ziperror); + zip_error_fini(&ziperror); + return false; + } + + //Add mimetype file NOTE: must be the first file in archive + const char mimetype[] = "application/vnd.oasis.opendocument.text"; + zip_source_t *source = zip_source_buffer(zipper, mimetype, sizeof (mimetype) - 1, 0); + if (source == nullptr) + { + qDebug() << "Failed to add file to zip:" << zip_strerror(zipper); + } + + if (zip_file_add(zipper, "mimetype", source, ZIP_FL_ENC_UTF_8) < 0) + { + zip_source_free(source); + qDebug() << "Failed to add file to zip:" << zip_strerror(zipper); + } + + //Add META-INF/manifest.xml + QString fileToCompress = dir.filePath(manifestFilePath); + + source = zip_source_file(zipper, fileToCompress.toUtf8(), 0, 0); + if (source == nullptr) + { + qDebug() << "Failed to add file to zip:" << zip_strerror(zipper); + } + + if (zip_file_add(zipper, manifestFilePath.data(), source, ZIP_FL_ENC_UTF_8) < 0) + { + zip_source_free(source); + qDebug() << "Failed to add file to zip:" << zip_strerror(zipper); + } + + //Add styles.xml + fileToCompress = dir.filePath(stylesFileName); + + source = zip_source_file(zipper, fileToCompress.toUtf8(), 0, 0); + if (source == nullptr) + { + qDebug() << "Failed to add file to zip:" << zip_strerror(zipper); + } + + if (zip_file_add(zipper, stylesFileName.data(), source, ZIP_FL_ENC_UTF_8) < 0) + { + zip_source_free(source); + qDebug() << "Failed to add file to zip:" << zip_strerror(zipper); + } + + //Add content.xml + fileToCompress = dir.filePath(contentFileName); + + source = zip_source_file(zipper, fileToCompress.toUtf8(), 0, 0); + if (source == nullptr) + { + qDebug() << "Failed to add file to zip:" << zip_strerror(zipper); + } + + if (zip_file_add(zipper, contentFileName.data(), source, ZIP_FL_ENC_UTF_8) < 0) + { + zip_source_free(source); + qDebug() << "Failed to add file to zip:" << zip_strerror(zipper); + } + + //Add meta.xml + fileToCompress = dir.filePath(metaFileName); + + source = zip_source_file(zipper, fileToCompress.toUtf8(), 0, 0); + if (source == nullptr) + { + qDebug() << "Failed to add file to zip:" << zip_strerror(zipper); + } + + if (zip_file_add(zipper, metaFileName.data(), source, ZIP_FL_ENC_UTF_8) < 0) + { + zip_source_free(source); + qDebug() << "Failed to add file to zip:" << zip_strerror(zipper); + } + + //Add possible images + QString imgBasePath = dir.filePath("Pictures") + QLatin1String("/%1"); + QString imgNewBasePath = QLatin1String("Pictures/%1"); + for(const auto& img : imageList) + { + source = zip_source_file(zipper, imgBasePath.arg(img.first).toUtf8(), 0, 0); + if (source == nullptr) + { + qDebug() << "Failed to add file to zip:" << zip_strerror(zipper); + } + + if (zip_file_add(zipper, imgNewBasePath.arg(img.first).toUtf8(), source, ZIP_FL_ENC_UTF_8) < 0) + { + zip_source_free(source); + qDebug() << "Failed to add file to zip:" << zip_strerror(zipper); + } + } + + zip_close(zipper); + return true; +} + +void OdtDocument::endDocument() +{ + saveManifest(dir.path()); + saveMeta(dir.path()); + + contentXml.writeEndDocument(); + content.close(); + + stylesXml.writeEndDocument(); + styles.close(); +} + +QString OdtDocument::addImage(const QString &name, const QString &mediaType) +{ + if(imageList.isEmpty()) + { + //First image added, create Pictures folder + QDir pictures(dir.path()); + if(!pictures.mkdir("Pictures")) + qWarning() << "OdtDocument: cannot create Pictures folder"; + } + imageList.append({name, mediaType}); + return dir.filePath("Pictures/" + name); +} + +void OdtDocument::writeStartDoc(QXmlStreamWriter &xml) +{ + xml.setAutoFormatting(true); + xml.setAutoFormattingIndent(-1); + //xml.writeStartDocument(QStringLiteral("1.0"), true); + xml.writeStartDocument(QStringLiteral("1.0")); +} + +void OdtDocument::writeFileEntry(QXmlStreamWriter& xml, + const QString& fullPath, const QString& mediaType) +{ + xml.writeStartElement("manifest:file-entry"); + xml.writeAttribute("manifest:full-path", fullPath); + xml.writeAttribute("manifest:media-type", mediaType); + xml.writeEndElement(); +} + +void OdtDocument::saveManifest(const QString& path) +{ + const QString xmlMime = QLatin1String("text/xml"); + + QDir manifestDir(path + metaInfDirPath); + if(!manifestDir.exists()) + manifestDir.mkpath("."); + + QFile manifest(dir.filePath(manifestFileName)); + manifest.open(QFile::WriteOnly | QFile::Truncate); + QXmlStreamWriter xml(&manifest); + writeStartDoc(xml); + + xml.writeStartElement("manifest:manifest"); + xml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:manifest:1.0", "manifest"); + xml.writeAttribute("manifest:version", "1.2"); + + //Root + writeFileEntry(xml, "/", "application/vnd.oasis.opendocument.text"); + + //styles.xml + writeFileEntry(xml, stylesFileName, xmlMime); + + //content.xml + writeFileEntry(xml, contentFileName, xmlMime); + + //meta.xml + writeFileEntry(xml, metaFileName, xmlMime); + + //Add possible images + for(const auto& img : imageList) + { + writeFileEntry(xml, "Pictures/" + img.first, img.second); + } + + xml.writeEndElement(); //manifest:manifest + + xml.writeEndDocument(); +} + +void OdtDocument::saveMeta(const QString& path) +{ + QDir metaDir(path); + + QFile metaFile(metaDir.filePath(metaFileName)); + metaFile.open(QFile::WriteOnly | QFile::Truncate); + QXmlStreamWriter xml(&metaFile); + writeStartDoc(xml); + + xml.writeStartElement("office:document-meta"); + xml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:office:1.0", "office"); + xml.writeNamespace("http://www.w3.org/1999/xlink", "xlink"); + xml.writeNamespace("http://purl.org/dc/elements/1.1/", "dc"); + xml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:meta:1.0", "meta"); + xml.writeNamespace("urn:oasis:names:tc:opendocument:xmlns:manifest:1.0", "manifest"); + xml.writeAttribute("office:version", "1.2"); + + xml.writeStartElement("office:meta"); + + MetaDataManager *meta = Session->getMetaDataManager(); + const bool storeLocationAndDate = AppSettings.getSheetStoreLocationDateInMeta(); + + //Title + if(!documentTitle.isEmpty()) + { + xml.writeStartElement("dc:title"); + xml.writeCharacters(documentTitle); + xml.writeEndElement(); //dc:title + } + + //Subject + xml.writeStartElement("dc:subject"); + xml.writeCharacters(AppDisplayName); + xml.writeCharacters(" Session Meeting"); //Do not translate, so it's standard for everyone + xml.writeEndElement(); //dc:subject + + //Description + QString meetingLocation; + if(storeLocationAndDate) + { + meta->getString(meetingLocation, MetaDataKey::MeetingLocation); + + QDate start, end; + qint64 tmp = 0; + if(meta->getInt64(tmp, MetaDataKey::MeetingStartDate) == MetaDataKey::ValueFound) + start = QDate::fromJulianDay(tmp); + if(meta->getInt64(tmp, MetaDataKey::MeetingEndDate) == MetaDataKey::ValueFound) + end = QDate::fromJulianDay(tmp); + if(!end.isValid() || end < start) + end = start; + + if(!meetingLocation.isEmpty() && start.isValid()) + { + //Store description only if metadata is valid + //Example: Meeting in CORNUDA from 07/11/2020 to 09/11/2020 + + QString description; + if(start != end) + { + description = Odt::tr("Meeting in %1 from %2 to %3") + .arg(meetingLocation) + .arg(start.toString("dd/MM/yyyy")) + .arg(end.toString("dd/MM/yyyy")); + } + else + { + description = Odt::tr("Meeting in %1 on %2") + .arg(meetingLocation) + .arg(start.toString("dd/MM/yyyy")); + } + + xml.writeStartElement("dc:description"); + xml.writeCharacters(description); + xml.writeEndElement(); //dc:description + } + } + + //Language + xml.writeStartElement("dc:language"); + xml.writeCharacters(AppSettings.getLanguage().name()); + xml.writeEndElement(); //dc:language + + //Generator + xml.writeStartElement("meta:generator"); + xml.writeCharacters(AppProduct); + xml.writeCharacters("/"); + xml.writeCharacters(AppVersion); + xml.writeCharacters("-"); + xml.writeCharacters(AppBuildDate); + xml.writeEndElement(); //meta:generator + + //Initial creator + xml.writeStartElement("meta:initial-creator"); + xml.writeCharacters(AppDisplayName); + xml.writeEndElement(); //meta:initial-creator + + //Creation date + xml.writeStartElement("meta:creation-date"); + //NOTE: date must be in ISO 8601 format but without time zone offset (LibreOffice doesn't recognize it) + // so do not use Qt::ISODate otherwise there is the risk of adding time zone offset to string + xml.writeCharacters(QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm:ss")); + xml.writeEndElement(); //meta:creation-date + + //Keywords + xml.writeStartElement("meta:keyword"); + xml.writeCharacters(AppDisplayName); + xml.writeEndElement(); //meta:keyword + + xml.writeStartElement("meta:keyword"); + xml.writeCharacters(AppProduct); + xml.writeEndElement(); //meta:keyword + + xml.writeStartElement("meta:keyword"); + xml.writeCharacters(AppCompany); + xml.writeEndElement(); //meta:keyword + + xml.writeStartElement("meta:keyword"); + xml.writeCharacters(Odt::tr("Meeting")); + xml.writeEndElement(); //meta:keyword + + xml.writeStartElement("meta:keyword"); + xml.writeCharacters("Meeting"); //Untranslated version + xml.writeEndElement(); //meta:keyword + + if(storeLocationAndDate && !meetingLocation.isEmpty()) + { + xml.writeStartElement("meta:keyword"); + xml.writeCharacters(meetingLocation); + xml.writeEndElement(); //meta:keyword + } + + //End + xml.writeEndElement(); //office:meta + xml.writeEndElement(); //office:document-meta + + xml.writeEndDocument(); +} diff --git a/src/odt_export/common/odtdocument.h b/src/odt_export/common/odtdocument.h new file mode 100644 index 0000000..c184376 --- /dev/null +++ b/src/odt_export/common/odtdocument.h @@ -0,0 +1,45 @@ +#ifndef ODTDOCUMENT_H +#define ODTDOCUMENT_H + +#include +#include +#include + +class OdtDocument +{ +public: + OdtDocument(); + + bool saveTo(const QString &fileName); + + bool initDocument(); + void startBody(); + void endDocument(); + + //Returns a 'path + file name' where you must save the image + QString addImage(const QString& name, const QString& mediaType); + + inline void setTitle(const QString& title) { documentTitle = title; } + +public: + QTemporaryDir dir; + QFile content; + QFile styles; + + QXmlStreamWriter contentXml; + QXmlStreamWriter stylesXml; + +private: + void writeStartDoc(QXmlStreamWriter &xml); + void saveMimetypeFile(const QString &path); + void saveManifest(const QString &path); + void saveMeta(const QString &path); + void writeFileEntry(QXmlStreamWriter &xml, const QString &fullPath, const QString &mediaType); + +private: + QString documentTitle; + //pair: fileName, mediaType + QList> imageList; +}; + +#endif // ODTDOCUMENT_H diff --git a/src/odt_export/common/odtutils.cpp b/src/odt_export/common/odtutils.cpp new file mode 100644 index 0000000..c7d048f --- /dev/null +++ b/src/odt_export/common/odtutils.cpp @@ -0,0 +1,360 @@ +#include "odtutils.h" + +/* writeColumnStyle + * + * Helper function to write table column style of a certain width + */ +void writeColumnStyle(QXmlStreamWriter &xml, const QString &name, const QString &width) +{ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-column"); + xml.writeAttribute("style:name", name); + + xml.writeStartElement("style:table-column-properties"); + xml.writeAttribute("style:column-width", width); + xml.writeEndElement(); //style:table-column-properties + xml.writeEndElement(); //style +} + +/* writeCell + * + * Helper function to write table cell + * Sets up the cell style and paragraph style, writes single line text and closes xml opened elements + */ +void writeCell(QXmlStreamWriter &xml, const QString &cellStyle, const QString ¶graphStyle, const QString &text) +{ + //Cell + xml.writeStartElement("table:table-cell"); + xml.writeAttribute("office:value-type", "string"); + xml.writeAttribute("table:style-name", cellStyle); + + //text:p + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", paragraphStyle); + xml.writeCharacters(text); + xml.writeEndElement(); //text:p + + xml.writeEndElement(); //table-cell +} + +/* writeCellListStart + * + * Helper function to write table cell but more advanced than writeCell + * Sets up the cell style and paragraph style + * Then you have full control on cell contents + * Then call writeCellListEnd + */ +void writeCellListStart(QXmlStreamWriter &xml, const QString &cellStyle, const QString ¶graphStyle) +{ + //Cell + xml.writeStartElement("table:table-cell"); + xml.writeAttribute("office:value-type", "string"); + xml.writeAttribute("table:style-name", cellStyle); + + //text:p + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", paragraphStyle); +} + +/* writeCellListEnd + * + * Helper function to close cell + */ +void writeCellListEnd(QXmlStreamWriter &xml) +{ + xml.writeEndElement(); //text:p + xml.writeEndElement(); //table-cell +} + +void writeStandardStyle(QXmlStreamWriter &xml) +{ + xml.writeStartElement("style:style"); + + xml.writeAttribute("style:name", "Standard"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:class", "text"); + + xml.writeEndElement(); //style:style +} + +void writeGraphicsStyle(QXmlStreamWriter &xml) +{ + xml.writeStartElement("style:style"); + + xml.writeAttribute("style:name", "Graphics"); + xml.writeAttribute("style:family", "graphic"); + + xml.writeStartElement("style:graphic-properties"); + xml.writeAttribute("svg:x", "0cm"); + xml.writeAttribute("svg:y", "0cm"); + xml.writeAttribute("style:vertical-pos", "top"); + xml.writeAttribute("style:vertical-rel", "paragraph"); + xml.writeAttribute("style:horizontal-pos", "center"); + xml.writeAttribute("style:horizontal-rel", "paragraph"); + xml.writeEndElement(); //style:graphic-properties + + xml.writeEndElement(); //style:style +} + +void writeCommonStyles(QXmlStreamWriter &xml) +{ + /* Style P1 + * type: paragraph style + * text-align: center + * font-size: 18pt + * font-weight: bold + * + * Usages: + * - JobWriter: + * - Job name (page title) + * - empty line for spacing after title + * - empty line for spacing after job_summary + * - empty line for spacing after job_stops + * + * - SessionRSWriter + * - Station or Rollingstock Owner name + * - empty line for spacing after rollingstock table + * + * - ShiftSheetExport: + * - Logo Picture text paragraph + * Could be any paragraph style, we just need a valid to draw a frame inside + * - Meeting dates + * - Meeting description + * + * - StationWriter: + * - Station name (page title) + * - empty line for spacing after title + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:name", "P1"); + + xml.writeStartElement("style:paragraph-properties"); + xml.writeAttribute("fo:text-align", "center"); + xml.writeAttribute("style:justify-single-word", "false"); + xml.writeEndElement(); //style:paragraph-properties + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("fo:font-size", "18pt"); + xml.writeAttribute("fo:font-weight", "bold"); + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style + + /* Style P4 + * type: paragraph style + * text-align: center + * font-size: 12pt + * font-weight: bold + * + * Desctription: + * - JobWriter: bold for headings + * - StationWriter: bold and a bit bigger than P3, used to emphatize Arrival or Departure + * + * Usages: + * - JobWriter: + * - job_stops table: Heading row cells (used for column names) + * + * - SessionRSWriter: + * - rollingstock table: Heading row cells (used for column names) + * - rollingstock table: content row cells + * + * - ShiftSheetExport + * - empty line for spacing at the very top of cover page (before Host Association name) + * - empty line for spacing before Logo Picture (after Host association) + * - empty line for spacing before Location (after Meeting dates) + * - empty line for spacing before description (after Location) + * - empty line for spacing before shift name box (after Description) + * + * - StationWriter: + * - stationtable table: Arrival for normal stop jobs + * - stationtable table: Departure on the repeated row for normal stop jobs + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:name", "P4"); + + xml.writeStartElement("style:paragraph-properties"); + xml.writeAttribute("fo:text-align", "center"); + xml.writeAttribute("style:justify-single-word", "false"); + xml.writeEndElement(); //style:paragraph-properties + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("fo:font-size", "12pt"); + xml.writeAttribute("fo:font-weight", "bold"); + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style + + /* Style T1 + * type: text style + * font-weight: bold + * + * Desctription: + * Set font to bold, inherit other properties from paragraph + * + * Usages: + * - JobWriter: bold for 'Cp:' and 'Unc:' rollingstock + * - StationWriter: bold for 'Cp:' and 'Unc:' rollingstock + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "text"); + xml.writeAttribute("style:name", "T1"); + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("fo:font-weight", "bold"); + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style +} + +void writeFooterStyle(QXmlStreamWriter &xml) +{ + //Base style for MP1 style used in header/footer + xml.writeStartElement("style:style"); + + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:class", "extra"); + xml.writeAttribute("style:name", "Footer"); + xml.writeAttribute("style:parent-style-name", "Standard"); + + xml.writeStartElement("style:paragraph-properties"); + + xml.writeAttribute("text:line-number", "0"); + xml.writeAttribute("text:number-lines", "false"); + + xml.writeStartElement("style:tab-stops"); + + xml.writeStartElement("style:tab-stop"); + xml.writeAttribute("style:position", "8.5cm"); + xml.writeAttribute("style:type", "center"); + xml.writeEndElement(); //style:tab-stop + + xml.writeStartElement("style:tab-stop"); + xml.writeAttribute("style:position", "17cm"); + xml.writeAttribute("style:type", "right"); + xml.writeEndElement(); //style:tab-stop + + xml.writeEndElement(); //style:tab-stops + + xml.writeEndElement(); //style:paragraph-properties + + xml.writeEndElement(); //style:style +} + +void writePageLayout(QXmlStreamWriter &xml) +{ + //Footer style + xml.writeStartElement("style:style"); + xml.writeAttribute("style:name", "MP1"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:parent-style-name", "Footer"); + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("style:font-name", "Liberation Sans"); + xml.writeEndElement(); //style:text-properties + xml.writeEndElement(); //style:style + + //Page Layout + xml.writeStartElement("style:page-layout"); + + xml.writeAttribute("style:name", "Mpm1"); + + xml.writeStartElement("style:page-layout-properties"); + xml.writeAttribute("fo:margin-top", "2cm"); + xml.writeAttribute("fo:margin-bottom", "2cm"); + xml.writeAttribute("fo:margin-left", "2cm"); + xml.writeAttribute("fo:margin-right", "2cm"); + xml.writeEndElement(); //style:page-layout-properties + + xml.writeStartElement("style:header-style"); + xml.writeEndElement(); //style:header-style + + xml.writeStartElement("style:footer-style"); + xml.writeEndElement(); //style:footer-style + + xml.writeEndElement(); //style:page-layout +} + +void writeHeaderFooter(QXmlStreamWriter &xml, const QString& headerText, const QString& footerText) +{ + xml.writeStartElement("style:master-page"); + + xml.writeAttribute("style:name", "Standard"); + xml.writeAttribute("style:page-layout-name", "Mpm1"); //TODO + + //Header + xml.writeStartElement("style:header"); + + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "MP1"); + + xml.writeCharacters(headerText); + + xml.writeStartElement("text:tab"); + xml.writeEndElement(); //text:tab + + xml.writeStartElement("text:tab"); + xml.writeEndElement(); //text:tab + + const QString pageStr = Odt::tr("Page "); + + xml.writeCharacters(pageStr); + + xml.writeStartElement("text:page-number"); + xml.writeAttribute("text:select-page", "current"); + xml.writeEndElement(); //text:page-number + + xml.writeEndElement(); //text:p + + xml.writeEndElement(); //style:header + + //Header for left pages (mirrored) + xml.writeStartElement("style:header-left"); + + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "MP1"); + + xml.writeCharacters(pageStr); + + xml.writeStartElement("text:page-number"); + xml.writeAttribute("text:select-page", "current"); + xml.writeEndElement(); //text:page-number + + xml.writeStartElement("text:tab"); + xml.writeEndElement(); //text:tab + + xml.writeStartElement("text:tab"); + xml.writeEndElement(); //text:tab + + xml.writeCharacters(headerText); + + xml.writeEndElement(); //text:p + + xml.writeEndElement(); //style:header-left + + //Footer + xml.writeStartElement("style:footer"); + + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "MP1"); + + xml.writeCharacters(footerText); + + xml.writeEndElement(); //text:p + + xml.writeEndElement(); //style:footer + + xml.writeEndElement(); //style:master-page +} + +void writeLiberationFontFaces(QXmlStreamWriter &xml) +{ + const QString variablePitch = QStringLiteral("variable"); + const QString liberationSerif = QStringLiteral("Liberation Serif"); + const QString liberationSans = QStringLiteral("Liberation Sans"); + const QString liberationMono = QStringLiteral("Liberation Mono"); + + writeFontFace(xml, liberationSerif, liberationSerif, "roman", variablePitch); + writeFontFace(xml, liberationSans, liberationSans, "swiss", variablePitch); + writeFontFace(xml, liberationMono, liberationMono, "modern", "fixed"); +} diff --git a/src/odt_export/common/odtutils.h b/src/odt_export/common/odtutils.h new file mode 100644 index 0000000..f966eac --- /dev/null +++ b/src/odt_export/common/odtutils.h @@ -0,0 +1,73 @@ +#ifndef ODTUTILS_H +#define ODTUTILS_H + +#include + +#include + +//small util +void writeColumnStyle(QXmlStreamWriter& xml, const QString& name, const QString& width); + +void writeCell(QXmlStreamWriter &xml, + const QString& cellStyle, + const QString& paragraphStyle, + const QString& text); + + +void writeCellListStart(QXmlStreamWriter &xml, const QString &cellStyle, const QString ¶graphStyle); +void writeCellListEnd(QXmlStreamWriter &xml); + +void writeStandardStyle(QXmlStreamWriter &xml); + +void writeGraphicsStyle(QXmlStreamWriter &xml); + +void writeCommonStyles(QXmlStreamWriter &xml); + +void writeFooterStyle(QXmlStreamWriter &xml); + +void writePageLayout(QXmlStreamWriter &xml); + +void writeHeaderFooter(QXmlStreamWriter &xml, + const QString& headerText, + const QString& footerText); + +inline void writeFontFace(QXmlStreamWriter &xml, + const QString& name, const QString& family, + const QString& genericFamily, const QString& pitch) +{ + xml.writeStartElement("style:font-face"); + xml.writeAttribute("style:name", name); + if(!family.isEmpty()) + { + QString familyQuoted; + familyQuoted.reserve(family.size() + 2); + bool needsQuotes = family.contains(' '); //If family name contains blanks + if(needsQuotes) //Enclose in single quotes + familyQuoted.append('\''); + familyQuoted.append(family); + if(needsQuotes) + familyQuoted.append('\''); + xml.writeAttribute("svg:font-family", familyQuoted); + } + if(!genericFamily.isEmpty()) + { + xml.writeAttribute("svg:font-family-generic", genericFamily); + } + if(!pitch.isEmpty()) + { + xml.writeAttribute("svg:font-pitch", pitch); + } + xml.writeEndElement(); //style:font-face +} + +void writeLiberationFontFaces(QXmlStreamWriter &xml); + +class Odt +{ + Q_DECLARE_TR_FUNCTIONS(Odt) +public: + static constexpr const char *CoupledAbbr = QT_TRANSLATE_NOOP("Odt", "Cp:"); + static constexpr const char *UncoupledAbbr = QT_TRANSLATE_NOOP("Odt", "Unc:"); +}; + +#endif // ODTUTILS_H diff --git a/src/odt_export/common/sessionrswriter.cpp b/src/odt_export/common/sessionrswriter.cpp new file mode 100644 index 0000000..4d961f5 --- /dev/null +++ b/src/odt_export/common/sessionrswriter.cpp @@ -0,0 +1,335 @@ +#include "sessionrswriter.h" + +#include "utils/platform_utils.h" +#include "utils/jobcategorystrings.h" +#include "utils/rs_utils.h" +#include "odtutils.h" + +#include + +SessionRSWriter::SessionRSWriter(database &db, SessionRSMode mode, SessionRSOrder order) : + lastParentId(0), + mDb(db), + q_getSessionRS(mDb), + q_getParentName(mDb), + m_mode(mode), + m_order(order) +{ + //TODO: fetch departure instead of arrival for start session + const auto sql = QStringLiteral("SELECT %1," + " %2, %3, %4," + " rs_list.id, rs_list.number, rs_models.name, rs_models.suffix, rs_models.type," + " stops.platform," + " stops.jobId, jobs.category, coupling.operation" + " FROM rs_list" + " JOIN coupling ON coupling.rsId=rs_list.id" + " JOIN stops ON stops.id=coupling.stopId" + " JOIN jobs ON jobs.id=stops.jobId" + " JOIN rs_models ON rs_models.id=rs_list.model_id" + " JOIN %5" + " GROUP BY rs_list.id" + " ORDER BY %6, stops.arrival, stops.jobId, rs_list.model_id"); + + QString temp = sql.arg(m_mode == SessionRSMode::StartOfSession ? "MIN(stops.arrival)" : "MAX(stops.departure)"); + if(m_order == SessionRSOrder::ByStation) + { + temp = temp + .arg("rs_list.owner_id") + .arg("rs_owners.name") + .arg("stops.stationId") + .arg("rs_owners ON rs_owners.id=rs_list.owner_id") + .arg("stops.stationId"); + + q_getParentName.prepare("SELECT name FROM stations WHERE id=?"); + } + else + { + temp = temp + .arg("stops.stationId") + .arg("stations.name") + .arg("rs_list.owner_id") + .arg("stations ON stations.id=stops.stationId") + .arg("rs_list.owner_id"); + + q_getParentName.prepare("SELECT name FROM rs_owners WHERE id=?"); + } + + QByteArray query = temp.toUtf8(); + + q_getSessionRS.prepare(query.constData()); +} + +void SessionRSWriter::writeStyles(QXmlStreamWriter &xml) +{ + /* Style P5 FIXME: merge with JobWriter and StationWriter + * type: paragraph + * text-align: center + * font-size: 12pt + * font-name: Liberation Sans + * + * Description: + * Like P4 but not bold, and Sans Serif + * + * Usages: + * - job_stops: stop cell text for normal stops and transit Rollingstock/Crossings/Passings/Description + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:name", "P5"); + + xml.writeStartElement("style:paragraph-properties"); + xml.writeAttribute("fo:text-align", "center"); + xml.writeAttribute("style:justify-single-word", "false"); + xml.writeEndElement(); //style:paragraph-properties + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("style:font-name", "Liberation Sans"); + xml.writeAttribute("fo:font-size", "12pt"); + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style + + + //rs_table Table + /* Style: rs_5f_table + * + * Type: table + * Display name: rollingstock + * Align: left + * Width: 16.0cm + * + * Usage: + * - SessionRSWriter: main table for Rollingstock Owners/Stations + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table"); + xml.writeAttribute("style:name", "rs_5f_table"); + xml.writeAttribute("style:display-name", "rollingstock"); + xml.writeStartElement("style:table-properties"); + xml.writeAttribute("style:shadow", "none"); + xml.writeAttribute("table:align", "left"); + xml.writeAttribute("style:width", "16.0cm"); + xml.writeEndElement(); //style:table-properties + xml.writeEndElement(); //style + + //rs_table columns + writeColumnStyle(xml, "rs_5f_table.A", "3.00cm"); //RS Name + writeColumnStyle(xml, "rs_5f_table.B", "4.45cm"); //Job + writeColumnStyle(xml, "rs_5f_table.C", "2.21cm"); //Platf + writeColumnStyle(xml, "rs_5f_table.D", "3.17cm"); //Departure or Arrival + writeColumnStyle(xml, "rs_5f_table.E", "4.00cm"); //Station or Owner + + /* Style: rs_5f_table.A1 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on left, top, bottom sides + * Padding: 0.049cm all sides + * + * Usage: + * - rs_5f_table table: top left/middle cells (except top right which has E1 style) + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "rs_5f_table.A1"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding", "0.049cm"); + xml.writeAttribute("fo:border-left", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-right", "none"); + xml.writeAttribute("fo:border-top", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-bottom", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + /* Style: rs_5f_table.E1 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on all sides + * Padding: 0.049cm all sides + * + * Usage: + * - rs_5f_table table: top right cell + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "rs_5f_table.E1"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding", "0.049cm"); + xml.writeAttribute("fo:border", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + /* Style: rs_5f_table.A2 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on left and bottom sides + * Padding: 0.049cm all sides + * + * Usage: + * - rs_5f_table table: right and middle cells from second row to last row + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "rs_5f_table.A2"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding", "0.049cm"); + xml.writeAttribute("fo:border-left", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-right", "none"); + xml.writeAttribute("fo:border-top", "none"); + xml.writeAttribute("fo:border-bottom", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + /* Style: rs_5f_table.E2 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on left, right and bottom sides + * Padding: 0.049cm all sides + * + * Usage: + * - rs_5f_table table: left cells from second row to last row + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "rs_5f_table.E2"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding", "0.049cm"); + xml.writeAttribute("fo:border-left", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-right", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-top", "none"); + xml.writeAttribute("fo:border-bottom", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style +} + +db_id SessionRSWriter::writeTable(QXmlStreamWriter &xml, const QString& parentName) +{ + //Table '???_table' where ??? is the station/owner name without spaces + + QString tableName = parentName; + tableName.replace(' ', '_'); //Replace spaces with underscores + tableName.append("_table"); + + xml.writeStartElement("table:table"); + xml.writeAttribute("table:name", tableName); + xml.writeAttribute("table:style-name", "rs_5f_table"); + + //Columns + xml.writeEmptyElement("table:table-column"); //A - RS Name + xml.writeAttribute("table:style-name", "rs_5f_table.A"); + + xml.writeEmptyElement("table:table-column"); //B - Job + xml.writeAttribute("table:style-name", "rs_5f_table.B"); + + xml.writeEmptyElement("table:table-column"); //C - Platf + xml.writeAttribute("table:style-name", "rs_5f_table.C"); + + xml.writeEmptyElement("table:table-column"); //D - Departure + xml.writeAttribute("table:style-name", "rs_5f_table.D"); + + xml.writeEmptyElement("table:table-column"); //E - Station or Owner + xml.writeAttribute("table:style-name", "rs_5f_table.E"); + + //Row 1 (Heading) + xml.writeStartElement("table:table-header-rows"); + xml.writeStartElement("table:table-row"); + + const QString P4_style = QStringLiteral("P4"); + const QString P5_style = QStringLiteral("P5"); + //Cells (column names, headings) + writeCell(xml, "rs_5f_table.A1", P4_style, Odt::tr("Rollingstock")); + writeCell(xml, "rs_5f_table.A1", P4_style, Odt::tr("Job")); + writeCell(xml, "rs_5f_table.A1", P4_style, Odt::tr("Platf")); + writeCell(xml, "rs_5f_table.A1", P4_style, m_mode == SessionRSMode::StartOfSession ? Odt::tr("Departure") : Odt::tr("Arrival")); + writeCell(xml, "rs_5f_table.E1", P4_style, m_order == SessionRSOrder::ByStation ? Odt::tr("Owner") : Odt::tr("Station")); + + xml.writeEndElement(); //end of row + xml.writeEndElement(); //header section + + //Fill the table + for(; it != q_getSessionRS.end(); ++it) + { + auto rs = *it; + QTime time = rs.get(0); //Departure or arrival + + //db_id stationOrOwnerId = rs.get(1); + QString name = rs.get(2); //Name of the station or owner + db_id parentId = rs.get(3); //ownerOrStation (opposite of stationOrOwner) + + //db_id rsId = rs.get(4); + int number = rs.get(5); + sqlite3_stmt *stmt = q_getSessionRS.stmt(); + int modelNameLen = sqlite3_column_bytes(stmt, 6); + const char *modelName = reinterpret_cast(sqlite3_column_text(stmt, 6)); + + int modelSuffixLen = sqlite3_column_bytes(stmt, 7); + const char *modelSuffix = reinterpret_cast(sqlite3_column_text(stmt, 7)); + RsType type = RsType(rs.get(8)); + + QString rsName = rs_utils::formatNameRef(modelName, modelNameLen, number, modelSuffix, modelSuffixLen, type); + + int platform = rs.get(9); + db_id jobId = rs.get(10); + JobCategory jobCat = JobCategory(rs.get(11)); + + if(parentId != lastParentId) + { + xml.writeEndElement(); //table:table + return parentId; + } + + xml.writeStartElement("table:table-row"); //start new row + + writeCell(xml, "rs_5f_table.A2", P5_style, rsName); + writeCell(xml, "rs_5f_table.A2", P5_style, JobCategoryName::jobName(jobId, jobCat)); + writeCell(xml, "rs_5f_table.A2", P5_style, utils::shortPlatformName(platform)); + writeCell(xml, "rs_5f_table.A2", P5_style, time.toString("HH:mm")); + writeCell(xml, "rs_5f_table.E2", P5_style, name); + + xml.writeEndElement(); //end of row + } + + xml.writeEndElement(); //table:table + + return 0; //End of document, no more tables +} + +void SessionRSWriter::writeContent(QXmlStreamWriter &xml) +{ + it = q_getSessionRS.begin(); + if(it == q_getSessionRS.end()) + return; + + lastParentId = (*it).get(3); + while (lastParentId) + { + q_getParentName.bind(1, lastParentId); + q_getParentName.step(); + QString name = q_getParentName.getRows().get(0); + q_getParentName.reset(); + + //Write Station or Rollingstock Owner name + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P1"); + xml.writeCharacters(name); + xml.writeEndElement(); + + lastParentId = writeTable(xml, name); + + //Add some space + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P1"); + xml.writeEndElement(); + } +} + +QString SessionRSWriter::generateTitle() const +{ + QString title = Odt::tr("Rollingstock by %1 at %2 of session") + .arg(m_order == SessionRSOrder::ByOwner ? Odt::tr("Owner") : Odt::tr("Station")) + .arg(m_mode == SessionRSMode::StartOfSession ? Odt::tr("start") : Odt::tr("end")); + return title; +} diff --git a/src/odt_export/common/sessionrswriter.h b/src/odt_export/common/sessionrswriter.h new file mode 100644 index 0000000..3c27a1d --- /dev/null +++ b/src/odt_export/common/sessionrswriter.h @@ -0,0 +1,38 @@ +#ifndef SESSIONRSWRITER_H +#define SESSIONRSWRITER_H + +#include "utils/types.h" +#include "utils/session_rs_modes.h" + +#include +using namespace sqlite3pp; + +class QXmlStreamWriter; + +class SessionRSWriter +{ +public: + SessionRSWriter(database &db, SessionRSMode mode, SessionRSOrder order); + + static void writeStyles(QXmlStreamWriter &xml); + + db_id writeTable(QXmlStreamWriter& xml, const QString& parentName); + + void writeContent(QXmlStreamWriter &xml); + + QString generateTitle() const; + +private: + db_id lastParentId; + + database &mDb; + + query q_getSessionRS; + query q_getParentName; + query::iterator it; + + SessionRSMode m_mode; + SessionRSOrder m_order; +}; + +#endif // SESSIONRSWRITER_H diff --git a/src/odt_export/common/stationwriter.cpp b/src/odt_export/common/stationwriter.cpp new file mode 100644 index 0000000..eb57276 --- /dev/null +++ b/src/odt_export/common/stationwriter.cpp @@ -0,0 +1,608 @@ +#include "stationwriter.h" + +#include "utils/platform_utils.h" + +#include "app/session.h" + +#include "utils/jobcategorystrings.h" +#include "utils/rs_utils.h" + +#include + +#include "odtutils.h" + +#include + +//first == true for the first time the stop is writter +//the second time it should be false to skip the repetition of Arrival, Crossings, Passings +void StationWriter::insertStop(QXmlStreamWriter &xml, const Stop& stop, bool first, bool transit) +{ + const QString P3_style = "P3"; + + //Row + xml.writeStartElement("table:table-row"); + + //Cells, hide Arrival the second time + if(first) + { + //Arrival in bold, if transit bold + italic + writeCell(xml, "stationtable.A2", transit ? "P5" : "P4", stop.arrival.toString("HH:mm")); + + //Departure, if transit don't repeat it + writeCell(xml, "stationtable.A2", P3_style, transit ? "--" : stop.departure.toString("HH:mm")); + } + else + { + writeCell(xml, "stationtable.A2", P3_style, QString()); //Don't repeat Arrival + writeCell(xml, "stationtable.A2", "P4", stop.departure.toString("HH:mm")); //Departure in bold + } + writeCell(xml, "stationtable.A2", P3_style, JobCategoryName::jobName(stop.jobId, stop.jobCat)); + writeCell(xml, "stationtable.A2", P3_style, utils::shortPlatformName(stop.platform)); + writeCell(xml, "stationtable.A2", P3_style, stop.prevSt); + writeCell(xml, "stationtable.A2", P3_style, stop.nextSt); + + if(!first) + { + //Fill with empty cells, needed to keep the order + writeCell(xml, "stationtable.A2", P3_style, QString()); //Rotabili + writeCell(xml, "stationtable.A2", P3_style, QString()); //Crossings + writeCell(xml, "stationtable.A2", P3_style, QString()); //Passings + writeCell(xml, "stationtable.L2", P3_style, QString()); //Note, descrizione + } +} + +StationWriter::StationWriter(database &db) : + mDb(db), + q_getJobsByStation(mDb, "SELECT stops.id," + "stops.jobId," + "jobs.category," + "stops.arrival," + "stops.departure," + "stops.platform," + "stops.transit," + "stops.description" + " FROM stops" + " JOIN jobs ON jobs.id=stops.jobId" + " WHERE stops.stationId=?" + " ORDER BY stops.arrival,stops.platform"), + + q_selectPassings(mDb, "SELECT stops.id,stops.jobid,jobs.category" + " FROM stops" + " JOIN jobs ON jobs.id=stops.jobId" + " WHERE stops.stationId=? AND stops.departure>=? AND stops.arrival<=? AND stops.jobId<>?"), + q_getStopCouplings(mDb, "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" + " JOIN rs_models ON rs_models.id=rs_list.model_id" + " WHERE coupling.stopId=? AND coupling.operation=?") +{ + +} + +//TODO: common styles with JobWriter should go in common +void StationWriter::writeStationAutomaticStyles(QXmlStreamWriter &xml) +{ + /* Style: stationtable + * + * Type: table + * Display name: Station Table + * Align: center + * Width: 20.0cm + * + * Usage: + * - StationWriter: station sheet main table showing jobs that stop in this station + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table"); + xml.writeAttribute("style:name", "stationtable"); + xml.writeAttribute("style:display-name", "Station Table"); + xml.writeStartElement("style:table-properties"); + xml.writeAttribute("style:shadow", "none"); + xml.writeAttribute("table:align", "center"); + xml.writeAttribute("style:width", "20.0cm"); + xml.writeEndElement(); //style:table-properties + xml.writeEndElement(); //style + + //stationtable columns + writeColumnStyle(xml, "stationtable.A", "1.74cm"); //1 Arrival + writeColumnStyle(xml, "stationtable.B", "1.80cm"); //2 Departure + writeColumnStyle(xml, "stationtable.C", "1.93cm"); //3 Job N + writeColumnStyle(xml, "stationtable.D", "1.00cm"); //4 Platform (Platf) + writeColumnStyle(xml, "stationtable.E", "3.09cm"); //5 From + writeColumnStyle(xml, "stationtable.F", "3.11cm"); //6 To + writeColumnStyle(xml, "stationtable.G", "3.00cm"); //7 Rollingstock + writeColumnStyle(xml, "stationtable.H", "2.10cm"); //8 Crossings + writeColumnStyle(xml, "stationtable.I", "2.10cm"); //9 Passings + writeColumnStyle(xml, "stationtable.L", "2.21cm"); //10 Description + + /* Style: stationtable.A1 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on left, top, bottom sides + * Padding: 0.097cm all sides + * + * Usage: + * - stationtable table: top left/middle cells (except top right which has L1 style) + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "stationtable.A1"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding", "0.097cm"); + xml.writeAttribute("fo:border-left", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-right", "none"); + xml.writeAttribute("fo:border-top", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-bottom", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + /* Style: stationtable.L1 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on all sides + * Padding: 0.097cm all sides + * + * Usage: + * - stationtable table: top right cell + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "stationtable.L1"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:border", "0.05pt solid #000000"); + xml.writeAttribute("fo:padding", "0.097cm"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + /* Style: stationtable.A2 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on left and bottom sides + * Padding: 0.097cm all sides + * + * Usage: + * - stationtable table: right and middle cells from second row to last row + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "stationtable.A2"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding", "0.097cm"); + xml.writeAttribute("fo:border-left", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-right", "none"); + xml.writeAttribute("fo:border-top", "none"); + xml.writeAttribute("fo:border-bottom", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + /* Style: stationtable.L2 + * + * Type: table-cell + * Border: 0.05pt solid #000000 on left, right and bottom sides + * Padding: 0.097cm all sides + * + * Usage: + * - stationtable table: left cells from second row to last row + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "table-cell"); + xml.writeAttribute("style:name", "stationtable.L2"); + + xml.writeStartElement("style:table-cell-properties"); + xml.writeAttribute("fo:padding", "0.097cm"); + xml.writeAttribute("fo:border-left", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-right", "0.05pt solid #000000"); + xml.writeAttribute("fo:border-top", "none"); + xml.writeAttribute("fo:border-bottom", "0.05pt solid #000000"); + xml.writeEndElement(); //style:table-cell-properties + xml.writeEndElement(); //style + + /* Style P2 + * type: paragraph + * text-align: center + * font-size: 13pt + * font-weight: bold + * + * Usages: + * - stationtable table: header cells + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:name", "P2"); + + xml.writeStartElement("style:paragraph-properties"); + xml.writeAttribute("fo:text-align", "center"); + xml.writeAttribute("style:justify-single-word", "false"); + xml.writeEndElement(); //style:paragraph-properties + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("fo:font-size", "13pt"); + xml.writeAttribute("fo:font-weight", "bold"); + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style + + /* Style P3 + * type: paragraph + * text-align: center + * font-size: 10pt + * + * Usages: + * - stationtable table: content cells + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:name", "P3"); + + xml.writeStartElement("style:paragraph-properties"); + xml.writeAttribute("fo:text-align", "center"); + xml.writeAttribute("style:justify-single-word", "false"); + xml.writeEndElement(); //style:paragraph-properties + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("fo:font-size", "10pt"); + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style + + //P5 - Bold, Italics, for Transits + /* Style P5 + * type: paragraph + * text-align: center + * font-size: 10pt + * font-weight: bold + * font-style: italic + * + * Usages: + * - stationtable table: Arrival for transit jobs (Normal stops have bold Arrival, transits have Bold + Italic) + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:name", "P5"); + + xml.writeStartElement("style:paragraph-properties"); + xml.writeAttribute("fo:text-align", "center"); + xml.writeAttribute("style:justify-single-word", "false"); + xml.writeEndElement(); //style:paragraph-properties + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("fo:font-size", "10pt"); + xml.writeAttribute("fo:font-weight", "bold"); + xml.writeAttribute("fo:font-style", "italic"); + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style +} + +void StationWriter::writeStation(QXmlStreamWriter &xml, db_id stationId, QString *stNameOut) +{ + QMap stops; //Order by Departure ASC + + query q_getStName(mDb, "SELECT name,short_name FROM stations WHERE id=?"); + + QString stationName; + QString shortName; + q_getStName.bind(1, stationId); + if(q_getStName.step() == SQLITE_ROW) + { + auto r = q_getStName.getRows(); + stationName = r.get(0); + shortName = r.get(1); + } + q_getStName.reset(); + if(stNameOut) + *stNameOut = stationName; + + //Title + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P1"); + xml.writeCharacters(Odt::tr("Station: %1%2") + .arg(stationName) + .arg(shortName.isEmpty() ? + QString() : + Odt::tr(" (%1)").arg(shortName))); + xml.writeEndElement(); + + //Vertical space + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P1"); + xml.writeEndElement(); + + //stationtable + xml.writeStartElement("table:table"); + //xml.writeAttribute("table:name", "stationtable"); + xml.writeAttribute("table:style-name", "stationtable"); + + xml.writeEmptyElement("table:table-column"); //Arrivo + xml.writeAttribute("table:style-name", "stationtable.A"); + + xml.writeEmptyElement("table:table-column"); //Partenza + xml.writeAttribute("table:style-name", "stationtable.B"); + + xml.writeEmptyElement("table:table-column"); //Treno N + xml.writeAttribute("table:style-name", "stationtable.C"); + + xml.writeEmptyElement("table:table-column"); //Binario (Bin) + xml.writeAttribute("table:style-name", "stationtable.D"); + + xml.writeEmptyElement("table:table-column"); //Proviene + xml.writeAttribute("table:style-name", "stationtable.E"); + + xml.writeEmptyElement("table:table-column"); //Parte + xml.writeAttribute("table:style-name", "stationtable.F"); + + xml.writeEmptyElement("table:table-column"); //Rotabili + xml.writeAttribute("table:style-name", "stationtable.G"); + + xml.writeEmptyElement("table:table-column"); //Crossings + xml.writeAttribute("table:style-name", "stationtable.H"); + + xml.writeEmptyElement("table:table-column"); //Passings + xml.writeAttribute("table:style-name", "stationtable.I"); + + xml.writeEmptyElement("table:table-column"); //Note + xml.writeAttribute("table:style-name", "stationtable.L"); + + //Row 1 (Heading) + xml.writeStartElement("table:table-header-rows"); + xml.writeStartElement("table:table-row"); + + writeCell(xml, "stationtable.A1", "P2", Odt::tr("Arrival")); + writeCell(xml, "stationtable.A1", "P2", Odt::tr("Departure")); + writeCell(xml, "stationtable.A1", "P2", Odt::tr("Job N")); + writeCell(xml, "stationtable.A1", "P2", Odt::tr("Platf")); + writeCell(xml, "stationtable.A1", "P2", Odt::tr("From")); + writeCell(xml, "stationtable.A1", "P2", Odt::tr("To")); + writeCell(xml, "stationtable.A1", "P2", Odt::tr("Rollingstock")); + writeCell(xml, "stationtable.A1", "P2", Odt::tr("Crossings")); + writeCell(xml, "stationtable.A1", "P2", Odt::tr("Passings")); + writeCell(xml, "stationtable.L1", "P2", Odt::tr("Notes")); //Description + + xml.writeEndElement(); //end of row + xml.writeEndElement(); //header section + + //Stops + q_getJobsByStation.bind(1, stationId); + for(auto r : q_getJobsByStation) + { + db_id stopId = r.get(0); + Stop stop; + stop.jobId = r.get(1); + stop.jobCat = JobCategory(r.get(2)); + stop.arrival = r.get(3); + stop.departure = r.get(4); + stop.platform = r.get(5); + bool isTransit = r.get(6); + stop.description = r.get(7); + + //BIG TODO: if this is First or Last stop of this job + //then it shouldn't be duplicated in 2 rows + + //Proviene, Parte + db_id tmpStId, unusedLine; + if(Session->getPrevStop(stopId, tmpStId, unusedLine)) + { + q_getStName.bind(1, tmpStId); + if(q_getStName.step() == SQLITE_ROW) + { + stop.prevSt = q_getStName.getRows().get(0); + } + q_getStName.reset(); + } + + if(Session->getNextStop(stopId, tmpStId, unusedLine)) + { + q_getStName.bind(1, tmpStId); + if(q_getStName.step() == SQLITE_ROW) + { + stop.nextSt = q_getStName.getRows().get(0); + } + q_getStName.reset(); + } + + for(auto s = stops.begin(); s != stops.end(); /*nothing because of erase*/) + { + //If 's' departs after 'stop' arrives then skip 's' for now + if(s->departure >= stop.arrival) + { + ++s; + continue; + } + + //'s' departs before 'stop' arrives so we + //insert it again to remind station master (IT: capostazione) + insertStop(xml, s.value(), false, false); + xml.writeEndElement(); //table-row + + //Then remove from the list otherwise it gets inserted infinite times + s = stops.erase(s); + } + + //Fill with basic data + insertStop(xml, stop, true, isTransit); + + //First time this stop is written, fill with other data + + //Rollingstock + sqlite3_stmt *stmt = q_getStopCouplings.stmt(); + writeCellListStart(xml, "stationtable.A2", "P3"); + + //Coupled rollingstock + bool firstCoupRow = true; + q_getStopCouplings.bind(1, stopId); + q_getStopCouplings.bind(2, RsOp::Coupled); + for(auto coup : q_getStopCouplings) + { + //db_id rsId = coup.get(0); + + int number = coup.get(1); + int modelNameLen = sqlite3_column_bytes(stmt, 2); + const char *modelName = reinterpret_cast(sqlite3_column_text(stmt, 2)); + + int modelSuffixLen = sqlite3_column_bytes(stmt, 3); + const char *modelSuffix = reinterpret_cast(sqlite3_column_text(stmt, 3)); + RsType type = RsType(sqlite3_column_int(stmt, 4)); + + const QString rsName = rs_utils::formatNameRef(modelName, modelNameLen, + number, + modelSuffix, modelSuffixLen, + type); + + if(firstCoupRow) + { + firstCoupRow = false; + //Use bold font + xml.writeStartElement("text:span"); + xml.writeAttribute("text:style-name", "T1"); + xml.writeCharacters(Odt::tr(Odt::CoupledAbbr)); + xml.writeEndElement(); //test:span + } + + xml.writeEmptyElement("text:line-break"); + xml.writeCharacters(rsName); + } + q_getStopCouplings.reset(); + + //Unoupled rollingstock + bool firstUncoupRow = true; + q_getStopCouplings.bind(1, stopId); + q_getStopCouplings.bind(2, RsOp::Uncoupled); + for(auto coup : q_getStopCouplings) + { + //db_id rsId = coup.get(0); + + int number = coup.get(1); + int modelNameLen = sqlite3_column_bytes(stmt, 2); + const char *modelName = reinterpret_cast(sqlite3_column_text(stmt, 2)); + + int modelSuffixLen = sqlite3_column_bytes(stmt, 3); + const char *modelSuffix = reinterpret_cast(sqlite3_column_text(stmt, 3)); + RsType type = RsType(sqlite3_column_int(stmt, 4)); + + const QString rsName = rs_utils::formatNameRef(modelName, modelNameLen, + number, + modelSuffix, modelSuffixLen, + type); + + if(firstUncoupRow) + { + if(!firstCoupRow) //Not first row, there were coupled rs + xml.writeEmptyElement("text:line-break"); //Separate from coupled + firstUncoupRow = false; + + //Use bold font + xml.writeStartElement("text:span"); + xml.writeAttribute("text:style-name", "T1"); + xml.writeCharacters(Odt::tr(Odt::UncoupledAbbr)); + xml.writeEndElement(); //test:span + } + + xml.writeEmptyElement("text:line-break"); + xml.writeCharacters(rsName); + } + q_getStopCouplings.reset(); + writeCellListEnd(xml); + + //Crossings, Passings + QVector passings; + Direction myDirection = Session->getStopDirection(stopId, stationId); + + q_selectPassings.bind(1, stationId); + q_selectPassings.bind(2, stop.arrival); + q_selectPassings.bind(3, stop.departure); + q_selectPassings.bind(4, stop.jobId); + firstCoupRow = true; + + //Incroci + writeCellListStart(xml, "stationtable.A2", "P3"); + for(auto pass : q_selectPassings) + { + db_id otherStopId = pass.get(0); + db_id otherJobId = pass.get(1); + JobCategory otherJobCat = JobCategory(pass.get(2)); + + //QTime otherArr = pass.get(3); + //QTime otherDep = pass.get(4); + + Direction otherDir = Session->getStopDirection(otherStopId, stationId); + + if(myDirection == otherDir) + passings.append({otherJobId, otherJobCat}); + else + { + if(firstCoupRow) + firstCoupRow = false; + else + xml.writeEmptyElement("text:line-break"); + xml.writeCharacters(JobCategoryName::jobName(otherJobId, otherJobCat)); + } + } + q_selectPassings.reset(); + writeCellListEnd(xml); + + //Passings + firstCoupRow = true; + writeCellListStart(xml, "stationtable.A2", "P3"); + for(auto entry : passings) + { + if(firstCoupRow) + firstCoupRow = false; + else + xml.writeEmptyElement("text:line-break"); + xml.writeCharacters(JobCategoryName::jobName(entry.jobId, entry.category)); + } + writeCellListEnd(xml); + + //Description, notes + writeCellListStart(xml, "stationtable.L2", "P3"); + if(isTransit) + { + xml.writeCharacters(Odt::tr("Transit")); + } + if(!stop.description.isEmpty()) + { + if(isTransit) //go to new line after 'Transit' word + xml.writeEmptyElement("text:line-break"); + + //Split in lines + int lastIdx = 0; + while(true) + { + int idx = stop.description.indexOf('\n', lastIdx); + QString line = stop.description.mid(lastIdx, idx == -1 ? idx : idx - lastIdx); + xml.writeCharacters(line.simplified()); + if(idx < 0) + break; //Last line + lastIdx = idx + 1; + xml.writeEmptyElement("text:line-break"); + } + } + writeCellListEnd(xml); + + xml.writeEndElement(); //table-row + + if(!isTransit) + { + //If it is a normal stop (not a transit) + //insert two rows: one for arrival and another + //that reminds the station master (capostazione) + //the departure of that train + //In order to achieve this we put the stop in a list + //and write it again before another arrival (see above) + stops.insert(stop.departure, stop); + } + } + q_getJobsByStation.reset(); + + for(const Stop& s : stops) + { + insertStop(xml, s, false, false); + xml.writeEndElement(); //table-row + } + + xml.writeEndElement(); //stationtable end +} diff --git a/src/odt_export/common/stationwriter.h b/src/odt_export/common/stationwriter.h new file mode 100644 index 0000000..ac43960 --- /dev/null +++ b/src/odt_export/common/stationwriter.h @@ -0,0 +1,48 @@ +#ifndef STATIONWRITER_H +#define STATIONWRITER_H + +#include +#include "utils/types.h" + +#include +using namespace sqlite3pp; + +class QXmlStreamWriter; + +class StationWriter +{ +public: + StationWriter(database& db); + + static void writeStationAutomaticStyles(QXmlStreamWriter& xml); + + void writeStation(QXmlStreamWriter &xml, db_id stationId, QString *stNameOut = nullptr); + +private: + typedef struct Stop + { + db_id jobId; + + QString prevSt; + QString nextSt; + + QString description; + + QTime arrival; + QTime departure; + + int platform; + JobCategory jobCat; + } Stop; + + void insertStop(QXmlStreamWriter &xml, const Stop& stop, bool first, bool transit); + +private: + database &mDb; + + query q_getJobsByStation; + query q_selectPassings; + query q_getStopCouplings; +}; + +#endif // STATIONWRITER_H diff --git a/src/odt_export/jobsheetexport.cpp b/src/odt_export/jobsheetexport.cpp new file mode 100644 index 0000000..c28641e --- /dev/null +++ b/src/odt_export/jobsheetexport.cpp @@ -0,0 +1,54 @@ +#include "jobsheetexport.h" + +#include "common/jobwriter.h" +#include "common/odtutils.h" + +#include "app/session.h" + +#include + +JobSheetExport::JobSheetExport(db_id jobId, JobCategory cat) : + m_jobId(jobId), + m_jobCat(cat) +{ + +} + +void JobSheetExport::write() +{ + qDebug() << "TEMP:" << odt.dir.path(); + odt.initDocument(); + + //styles.xml font declarations + odt.stylesXml.writeStartElement("office:font-face-decls"); + writeLiberationFontFaces(odt.stylesXml); + odt.stylesXml.writeEndElement(); //office:font-face-decls + + //Content font declarations + odt.contentXml.writeStartElement("office:font-face-decls"); + writeLiberationFontFaces(odt.contentXml); + odt.contentXml.writeEndElement(); //office:font-face-decls + + //Content Automatic styles + odt.contentXml.writeStartElement("office:automatic-styles"); + JobWriter::writeJobAutomaticStyles(odt.contentXml); + + //Styles + odt.stylesXml.writeStartElement("office:styles"); + writeCommonStyles(odt.stylesXml); + JobWriter::writeJobStyles(odt.stylesXml); + odt.stylesXml.writeEndElement(); + + //Body + odt.startBody(); + + JobWriter w(Session->m_Db); + w.writeJob(odt.contentXml, m_jobId, m_jobCat); + + odt.endDocument(); +} + +void JobSheetExport::save(const QString &fileName) +{ + odt.saveTo(fileName); +} diff --git a/src/odt_export/jobsheetexport.h b/src/odt_export/jobsheetexport.h new file mode 100644 index 0000000..7d36c94 --- /dev/null +++ b/src/odt_export/jobsheetexport.h @@ -0,0 +1,24 @@ +#ifndef JOBSHEETEXPORT_H +#define JOBSHEETEXPORT_H + +#include "common/odtdocument.h" + +#include "utils/types.h" + + +class JobSheetExport +{ +public: + JobSheetExport(db_id jobId, JobCategory cat); + + void write(); + void save(const QString& fileName); + +private: + OdtDocument odt; + + db_id m_jobId; + JobCategory m_jobCat; +}; + +#endif // JOBSHEETEXPORT_H diff --git a/src/odt_export/sessionrsexport.cpp b/src/odt_export/sessionrsexport.cpp new file mode 100644 index 0000000..4b202a2 --- /dev/null +++ b/src/odt_export/sessionrsexport.cpp @@ -0,0 +1,84 @@ +#include "sessionrsexport.h" + +#include "common/odtutils.h" +#include "common/sessionrswriter.h" + +#include "app/session.h" +#include "db_metadata/metadatamanager.h" + +#include + +SessionRSExport::SessionRSExport(SessionRSMode mode, SessionRSOrder order) : + m_mode(mode), + m_order(order) +{ + +} + +void SessionRSExport::write() +{ + qDebug() << "TEMP:" << odt.dir.path(); + odt.initDocument(); + + //styles.xml font declarations + odt.stylesXml.writeStartElement("office:font-face-decls"); + writeLiberationFontFaces(odt.stylesXml); + odt.stylesXml.writeEndElement(); //office:font-face-decls + + //Styles + odt.stylesXml.writeStartElement("office:styles"); + writeStandardStyle(odt.stylesXml); + writeCommonStyles(odt.stylesXml); + //JobWriter::writeJobStyles(odt.stylesXml); TODO + writeFooterStyle(odt.stylesXml); + odt.stylesXml.writeEndElement(); + + //Automatic styles + odt.stylesXml.writeStartElement("office:automatic-styles"); + writePageLayout(odt.stylesXml); + odt.stylesXml.writeEndElement(); + + MetaDataManager *meta = Session->getMetaDataManager(); + + //Retrive header and footer: give precedence to database metadata and then fallback to global application settings + //If the text was explicitly set to empty in metadata no header/footer will be displayed + QString header; + if(meta->getString(header, MetaDataKey::SheetHeaderText) != MetaDataKey::Result::ValueFound) + { + header = AppSettings.getSheetHeader(); + } + + QString footer; + if(meta->getString(footer, MetaDataKey::SheetFooterText) != MetaDataKey::Result::ValueFound) + { + footer = AppSettings.getSheetFooter(); + } + + //Master styles + odt.stylesXml.writeStartElement("office:master-styles"); + writeHeaderFooter(odt.stylesXml, header, footer); + odt.stylesXml.writeEndElement(); + + //Content font declarations + odt.contentXml.writeStartElement("office:font-face-decls"); + writeLiberationFontFaces(odt.contentXml); + odt.contentXml.writeEndElement(); //office:font-face-decls + + //Content Automatic styles + odt.contentXml.writeStartElement("office:automatic-styles"); + SessionRSWriter::writeStyles(odt.contentXml); + + //Body + odt.startBody(); + + SessionRSWriter w(Session->m_Db, m_mode, m_order); + odt.setTitle(w.generateTitle()); + w.writeContent(odt.contentXml); + + odt.endDocument(); +} + +void SessionRSExport::save(const QString &fileName) +{ + odt.saveTo(fileName); +} diff --git a/src/odt_export/sessionrsexport.h b/src/odt_export/sessionrsexport.h new file mode 100644 index 0000000..be5b839 --- /dev/null +++ b/src/odt_export/sessionrsexport.h @@ -0,0 +1,24 @@ +#ifndef SESSIONRSEXPORT_H +#define SESSIONRSEXPORT_H + +#include "common/odtdocument.h" + +#include "utils/types.h" +#include "utils/session_rs_modes.h" //TODO: complete + +class SessionRSExport +{ +public: + SessionRSExport(SessionRSMode mode, SessionRSOrder order); + + void write(); + void save(const QString& fileName); + +private: + OdtDocument odt; + + SessionRSMode m_mode; + SessionRSOrder m_order; +}; + +#endif // SESSIONRSEXPORT_H diff --git a/src/odt_export/shiftsheetexport.cpp b/src/odt_export/shiftsheetexport.cpp new file mode 100644 index 0000000..e919fcd --- /dev/null +++ b/src/odt_export/shiftsheetexport.cpp @@ -0,0 +1,482 @@ +#include "shiftsheetexport.h" + +#include "common/jobwriter.h" + +#include "common/odtutils.h" + +#include "app/session.h" +#include "db_metadata/metadatamanager.h" +#include "db_metadata/imagemetadata.h" + +#include + +#include + +#include + +ShiftSheetExport::ShiftSheetExport(db_id shiftId) : + m_shiftd(shiftId), + + q_getShiftJobs(Session->m_Db, + "SELECT jobs.id,jobs.category,s1.arrival" + " FROM jobs" + " JOIN stops s1 ON s1.id=jobs.firstStop" + " WHERE jobs.shiftId=? ORDER BY s1.arrival ASC"), + logoWidthCm(0), + logoHeightCm(0) +{ + +} + +void ShiftSheetExport::write() +{ + qDebug() << "TEMP:" << odt.dir.path(); + odt.initDocument(); + + //styles.xml font declarations + odt.stylesXml.writeStartElement("office:font-face-decls"); + writeLiberationFontFaces(odt.stylesXml); + odt.stylesXml.writeEndElement(); //office:font-face-decls + + //Styles + odt.stylesXml.writeStartElement("office:styles"); + writeStandardStyle(odt.stylesXml); + writeGraphicsStyle(odt.stylesXml); + writeCommonStyles(odt.stylesXml); + JobWriter::writeJobStyles(odt.stylesXml); + writeFooterStyle(odt.stylesXml); + odt.stylesXml.writeEndElement(); + + //Automatic styles + odt.stylesXml.writeStartElement("office:automatic-styles"); + writePageLayout(odt.stylesXml); + odt.stylesXml.writeEndElement(); + + MetaDataManager *meta = Session->getMetaDataManager(); + + //Retrive header and footer: give precedence to database metadata and then fallback to global application settings + //If the text was explicitly set to empty in metadata no header/footer will be displayed + QString header; + if(meta->getString(header, MetaDataKey::SheetHeaderText) != MetaDataKey::Result::ValueFound) + { + header = AppSettings.getSheetHeader(); + } + + QString footer; + if(meta->getString(footer, MetaDataKey::SheetFooterText) != MetaDataKey::Result::ValueFound) + { + footer = AppSettings.getSheetFooter(); + } + + //Master styles + odt.stylesXml.writeStartElement("office:master-styles"); + writeHeaderFooter(odt.stylesXml, header, footer); + odt.stylesXml.writeEndElement(); + + //Content font declarations + odt.contentXml.writeStartElement("office:font-face-decls"); + writeLiberationFontFaces(odt.contentXml); + odt.contentXml.writeEndElement(); //office:font-face-decls + + //Content Automatic styles + odt.contentXml.writeStartElement("office:automatic-styles"); + JobWriter::writeJobAutomaticStyles(odt.contentXml); + + bool hasLogo = (meta->hasKey(MetaDataKey::MeetingLogoPicture) == MetaDataKey::ValueFound); + if(hasLogo) + { + //Save image + saveLogoPicture(); + } + writeCoverStyles(odt.contentXml, hasLogo); + + //Body + odt.startBody(); + + QString shiftName; + { + query q_getShiftName(Session->m_Db, "SELECT name FROM jobshifts WHERE id=?"); + q_getShiftName.bind(1, m_shiftd); + if(q_getShiftName.step() == SQLITE_ROW) + shiftName = q_getShiftName.getRows().get(0); + } + odt.setTitle(Odt::tr("Shift %1").arg(shiftName)); + + writeCover(odt.contentXml, shiftName, hasLogo); + + JobWriter w(Session->m_Db); + + q_getShiftJobs.bind(1, m_shiftd); + for(auto r : q_getShiftJobs) + { + db_id jobId = r.get(0); + JobCategory cat = JobCategory(r.get(1)); + w.writeJob(odt.contentXml, jobId, cat); + } + + odt.endDocument(); +} + +void ShiftSheetExport::save(const QString &fileName) +{ + odt.saveTo(fileName); +} + +void ShiftSheetExport::writeCoverStyles(QXmlStreamWriter &xml, bool hasImage) +{ + if(hasImage) + { + /* Style logo_style + * + * type: graphic + * vertical-pos: top + * horizontal-pos: center + * + * Usages: + * - ShiftSheetExport: cover page logo picture frame + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "graphic"); + xml.writeAttribute("style:name", "logo_style"); + xml.writeAttribute("style:parent-style-name", "Graphics"); + + xml.writeStartElement("style:graphic-properties"); + xml.writeAttribute("style:vertical-rel", "paragraph"); + xml.writeAttribute("style:vertical-pos", "top"); + xml.writeAttribute("style:horizontal-rel", "paragraph"); + xml.writeAttribute("style:horizontal-pos", "center"); + xml.writeAttribute("style:wrap", "none"); + xml.writeAttribute("draw:color-mode", "standard"); + xml.writeAttribute("draw:color-inversion", "false"); + xml.writeAttribute("draw:red", "0%"); + xml.writeAttribute("draw:green", "0%"); + xml.writeAttribute("draw:blue", "0%"); + xml.writeAttribute("draw:contrast", "0%"); + xml.writeAttribute("draw:gamma", "100%"); + xml.writeAttribute("draw:luminance", "0%"); + xml.writeAttribute("draw:image-opacity", "100%"); + xml.writeAttribute("fo:clip", "rect(0cm, 0cm, 0cm, 0cm)"); + xml.writeEndElement(); //style:graphic-properties + xml.writeEndElement(); //style + } + + /* Style P7 + * type: paragraph + * text-align: center + * font-size: 24pt + * font-weight: bold + * font-name: Liberation Serif + * + * Usages: + * - ShiftSheetExport: cover page for host association name + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:name", "P7"); + + xml.writeStartElement("style:paragraph-properties"); + xml.writeAttribute("fo:text-align", "center"); + xml.writeAttribute("style:justify-single-word", "false"); + xml.writeEndElement(); //style:paragraph-properties + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("style:font-name", "Liberation Serif"); + xml.writeAttribute("fo:font-size", "24pt"); + xml.writeAttribute("fo:font-weight", "bold"); + + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style + + /* Style P8 + * type: paragraph + * text-align: center + * font-size: 24pt + * font-weight: bold + * font-name: Liberation Sans + * + * Description: + * Like P7 but Sans Serif font + * + * Usages: + * - ShiftSheetExport: cover page for location name + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:name", "P8"); + + xml.writeStartElement("style:paragraph-properties"); + xml.writeAttribute("fo:text-align", "center"); + xml.writeAttribute("style:justify-single-word", "false"); + xml.writeEndElement(); //style:paragraph-properties + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("style:font-name", "Liberation Sans"); + xml.writeAttribute("fo:font-size", "24pt"); + xml.writeAttribute("fo:font-weight", "bold"); + + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style + + /* Style shift_name_style + * type: paragraph + * text-align: center + * font-size: 20pt + * font-weight: bold + * padding 0.50cm left/right, 0.10cm top, 0.15cm bottom + * border: 0.05pt solid #000000 on all sides + * font-name: Liberation Sans + * + * Usages: + * - ShiftSheetExport: cover page for shift name box + */ + xml.writeStartElement("style:style"); + xml.writeAttribute("style:family", "paragraph"); + xml.writeAttribute("style:name", "shift_name_style"); + + xml.writeStartElement("style:paragraph-properties"); + xml.writeAttribute("fo:text-align", "center"); + xml.writeAttribute("style:justify-single-word", "false"); + xml.writeEndElement(); //style:paragraph-properties + + xml.writeStartElement("style:text-properties"); + xml.writeAttribute("style:font-name", "Liberation Sans"); + xml.writeAttribute("fo:font-size", "32pt"); + xml.writeAttribute("fo:font-weight", "bold"); + + xml.writeAttribute("fo:padding-left", "0.50cm"); + xml.writeAttribute("fo:padding-right", "0.50cm"); + xml.writeAttribute("fo:padding-top", "0.10cm"); + xml.writeAttribute("fo:padding-bottom", "0.15cm"); + xml.writeAttribute("fo:border", "0.05pt solid #000000"); + + xml.writeEndElement(); //style:text-properties + + xml.writeEndElement(); //style:style +} + +bool ShiftSheetExport::calculateLogoSize(QIODevice *dev) +{ + QImageReader reader(dev, "PNG"); + if(!reader.canRead()) + { + qWarning() << "ShiftSheetExport: cannot read picture," << reader.errorString(); + return false; + } + QImage img; + if(!reader.read(&img)) + { + qWarning() << "ShiftSheetExport: cannot read picture," << reader.errorString(); + return false; + } + + const int widthPx = img.width(); + const int heightPx = img.height(); + + const double dotsX = img.dotsPerMeterX(); + const double dotsY = img.dotsPerMeterY(); + + logoWidthCm = 100.0 * double(widthPx) / dotsX; + logoHeightCm = 100.0 * double(heightPx) / dotsY; + + if(logoWidthCm > 20.0 || logoWidthCm < 2.0) + { + //Try with 15 cm wide + logoWidthCm = 15.0; + logoHeightCm = 15.0 * double(heightPx)/double(widthPx); + } + + if(logoHeightCm > 10.0) + { + //Maximum 10 cm tall + logoWidthCm = 10.0 * double(widthPx)/double(heightPx); + logoHeightCm = 10.0; + } + + return true; +} + +void ShiftSheetExport::saveLogoPicture() +{ + std::unique_ptr imageIO; + imageIO.reset(ImageMetaData::getImage(Session->m_Db, MetaDataKey::MeetingLogoPicture)); + if(!imageIO || !imageIO->open(QIODevice::ReadOnly)) + { + qWarning() << "ShiftSheetExport: error query image," << Session->m_Db.error_msg(); + return; + } + + if(!calculateLogoSize(imageIO.get())) + return; //Image is not valid + + imageIO->seek(0); //Reset device + + QString fileNamePath = odt.addImage("logo.png", "image/png"); + QFile f(fileNamePath); + if(!f.open(QFile::WriteOnly | QFile::Truncate)) + { + qWarning() << "ShiftSheetExport: error saving image," << f.errorString(); + return; + } + + constexpr int bufSize = 4096; + char buf[bufSize]; + qint64 size = 0; + while (!imageIO->atEnd() || size < 0) + { + size = imageIO->read(buf, bufSize); + if(f.write(buf, size) != size) + qWarning() << "ShiftSheetExport: error witing image," << f.errorString(); + } + f.close(); +} + +void ShiftSheetExport::writeCover(QXmlStreamWriter& xml, const QString& shiftName, bool hasLogo) +{ + MetaDataManager *meta = Session->getMetaDataManager(); + + //Add some space + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P4"); + xml.writeEndElement(); + + //Host association + QString str; + meta->getString(str, MetaDataKey::MeetingHostAssociation); + if(!str.isEmpty()) + { + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P7"); + xml.writeCharacters(str); + xml.writeEndElement(); + str.clear(); + } + + //Add some space + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P4"); + xml.writeEndElement(); + + //Logo + if(hasLogo && !qFuzzyIsNull(logoWidthCm)) + { + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P1"); + + xml.writeStartElement("draw:frame"); + xml.writeAttribute("draw:name", "meeting_logo"); + xml.writeAttribute("draw:style-name", "logo_style"); + xml.writeAttribute("text:anchor-type", "paragraph"); + xml.writeAttribute("draw:z-index", "0"); + + const QString cmFmt = QStringLiteral("%1cm"); + xml.writeAttribute("svg:width", cmFmt.arg(logoWidthCm, 0, 'f', 3)); + xml.writeAttribute("svg:height", cmFmt.arg(logoHeightCm, 0, 'f', 3)); + + xml.writeStartElement("draw:image"); + xml.writeAttribute("xlink:href", "Pictures/logo.png"); + xml.writeAttribute("xlink:type", "simple"); + xml.writeAttribute("xlink:show", "embed"); + xml.writeAttribute("xlink:actuate", "onLoad"); + xml.writeEndElement(); //draw:image + + xml.writeEndElement(); //draw:frame + + xml.writeEndElement(); //text:p + } + + //Meeting dates + qint64 showDates = 1; + meta->getInt64(showDates, MetaDataKey::MeetingShowDates); + if(showDates) + { + QDate start, end; + qint64 tmp = 0; + + if(meta->getInt64(tmp, MetaDataKey::MeetingStartDate) == MetaDataKey::ValueFound) + { + start = QDate::fromJulianDay(tmp); + } + if(meta->getInt64(tmp, MetaDataKey::MeetingEndDate) == MetaDataKey::ValueFound) + { + end = QDate::fromJulianDay(tmp); + if(!end.isValid() || end < start) + end = start; + } + + if(start.isValid()) + { + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P1"); + if(start == end) + xml.writeCharacters(start.toString("dd/MM/yyyy")); + else + xml.writeCharacters(Odt::tr("From %1 to %2") + .arg(start.toString("dd/MM/yyyy")) + .arg(end.toString("dd/MM/yyyy"))); + xml.writeEndElement(); + } + } + + //Add some space + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P4"); + xml.writeEndElement(); + + //Location + meta->getString(str, MetaDataKey::MeetingLocation); + if(!str.isEmpty()) + { + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P8"); + xml.writeCharacters(str); + xml.writeEndElement(); + str.clear(); + } + + //Add some space + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P4"); + xml.writeEndElement(); + + //Description + meta->getString(str, MetaDataKey::MeetingDescription); + if(!str.isEmpty()) + { + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P1"); + + //Split in lines + int lastIdx = 0; + while(true) + { + int idx = str.indexOf('\n', lastIdx); + QString line = str.mid(lastIdx, idx == -1 ? idx : idx - lastIdx); + xml.writeCharacters(line.simplified()); + if(idx < 0) + break; //Last line + lastIdx = idx + 1; + xml.writeEmptyElement("text:line-break"); + } + xml.writeEndElement(); + str.clear(); + } + + //Add some space + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "P4"); + xml.writeEndElement(); + + //Shift name + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "shift_name_style"); + xml.writeCharacters(Odt::tr("SHIFT %1").arg(shiftName)); + xml.writeEndElement(); + str.clear(); + + //Interruzione pagina TODO: see style 'interruzione' + xml.writeStartElement("text:p"); + xml.writeAttribute("text:style-name", "interruzione"); + xml.writeEndElement(); +} diff --git a/src/odt_export/shiftsheetexport.h b/src/odt_export/shiftsheetexport.h new file mode 100644 index 0000000..6a1331c --- /dev/null +++ b/src/odt_export/shiftsheetexport.h @@ -0,0 +1,38 @@ +#ifndef SHIFTSHEETEXPORT_H +#define SHIFTSHEETEXPORT_H + +#include "common/odtdocument.h" + +#include "utils/types.h" + +#include +using namespace sqlite3pp; + +class ShiftSheetExport +{ +public: + ShiftSheetExport(db_id shiftId); + + void write(); + void save(const QString& fileName); + + inline void setShiftId(db_id shiftId) { m_shiftd = shiftId; } + +private: + void writeCoverStyles(QXmlStreamWriter &xml, bool hasImage); + bool calculateLogoSize(QIODevice *dev); + void saveLogoPicture(); + void writeCover(QXmlStreamWriter &xml, const QString &shiftName, bool hasLogo); + +private: + OdtDocument odt; + + db_id m_shiftd; + + query q_getShiftJobs; + + double logoWidthCm; + double logoHeightCm; +}; + +#endif // SHIFTSHEETEXPORT_H diff --git a/src/odt_export/stationsheetexport.cpp b/src/odt_export/stationsheetexport.cpp new file mode 100644 index 0000000..f8bbf7c --- /dev/null +++ b/src/odt_export/stationsheetexport.cpp @@ -0,0 +1,84 @@ +#include "stationsheetexport.h" + +#include "common/stationwriter.h" + +#include "common/odtutils.h" + +#include "app/session.h" +#include "db_metadata/metadatamanager.h" + +#include + +StationSheetExport::StationSheetExport(db_id stationId) : + m_stationId(stationId) +{ + +} + +void StationSheetExport::write() +{ + qDebug() << "TEMP:" << odt.dir.path(); + odt.initDocument(); + + //styles.xml font declarations + odt.stylesXml.writeStartElement("office:font-face-decls"); + writeLiberationFontFaces(odt.stylesXml); + odt.stylesXml.writeEndElement(); //office:font-face-decls + + //Styles + odt.stylesXml.writeStartElement("office:styles"); + writeStandardStyle(odt.stylesXml); + writeFooterStyle(odt.stylesXml); + odt.stylesXml.writeEndElement(); + + //Automatic styles + odt.stylesXml.writeStartElement("office:automatic-styles"); + writePageLayout(odt.stylesXml); + odt.stylesXml.writeEndElement(); + + MetaDataManager *meta = Session->getMetaDataManager(); + + //Retrive header and footer: give precedence to database metadata and then fallback to global application settings + //If the text was explicitly set to empty in metadata no header/footer will be displayed + QString header; + if(meta->getString(header, MetaDataKey::SheetHeaderText) != MetaDataKey::Result::ValueFound) + { + header = AppSettings.getSheetHeader(); + } + + QString footer; + if(meta->getString(footer, MetaDataKey::SheetFooterText) != MetaDataKey::Result::ValueFound) + { + footer = AppSettings.getSheetFooter(); + } + + //Master styles + odt.stylesXml.writeStartElement("office:master-styles"); + writeHeaderFooter(odt.stylesXml, header, footer); + odt.stylesXml.writeEndElement(); + + //Content font declarations + odt.contentXml.writeStartElement("office:font-face-decls"); + writeLiberationFontFaces(odt.contentXml); + odt.contentXml.writeEndElement(); //office:font-face-decls + + //Content Automatic styles + odt.contentXml.writeStartElement("office:automatic-styles"); + writeCommonStyles(odt.contentXml); + StationWriter::writeStationAutomaticStyles(odt.contentXml); + + //Body + odt.startBody(); + + StationWriter w(Session->m_Db); + QString stName; + w.writeStation(odt.contentXml, m_stationId, &stName); + odt.setTitle(Odt::tr("%1 station").arg(stName)); + + odt.endDocument(); +} + +void StationSheetExport::save(const QString &fileName) +{ + odt.saveTo(fileName); +} diff --git a/src/odt_export/stationsheetexport.h b/src/odt_export/stationsheetexport.h new file mode 100644 index 0000000..fe63356 --- /dev/null +++ b/src/odt_export/stationsheetexport.h @@ -0,0 +1,23 @@ +#ifndef STATIONSHEETEXPORT_H +#define STATIONSHEETEXPORT_H + +#include "common/odtdocument.h" + +#include "utils/types.h" + +//TODO: implement, deprecate MeetingSession API +class StationSheetExport +{ +public: + StationSheetExport(db_id stationId); + + void write(); + void save(const QString& fileName); + +private: + OdtDocument odt; + + db_id m_stationId; +}; + +#endif // STATIONSHEETEXPORT_H diff --git a/src/printing/CMakeLists.txt b/src/printing/CMakeLists.txt new file mode 100644 index 0000000..84704cf --- /dev/null +++ b/src/printing/CMakeLists.txt @@ -0,0 +1,24 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + printing/fileoptionspage.h + printing/printdefs.h + printing/printeroptionspage.h + printing/printwizard.h + printing/printworker.h + printing/progresspage.h + printing/selectionpage.h + + printing/fileoptionspage.cpp + printing/printeroptionspage.cpp + printing/printwizard.cpp + printing/printworker.cpp + printing/progresspage.cpp + printing/selectionpage.cpp + PARENT_SCOPE +) + +set(TRAINTIMETABLE_UI_FILES + ${TRAINTIMETABLE_UI_FILES} + printing/pdfoptionspage.ui + PARENT_SCOPE +) diff --git a/src/printing/fileoptionspage.cpp b/src/printing/fileoptionspage.cpp new file mode 100644 index 0000000..3598488 --- /dev/null +++ b/src/printing/fileoptionspage.cpp @@ -0,0 +1,188 @@ +#include "fileoptionspage.h" + +#include "printwizard.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include "utils/file_format_names.h" + +#include + +#include + +FileOptionsPage::FileOptionsPage(PrintWizard *w, QWidget *parent) : + QWizardPage (parent), + mWizard(w) +{ + createFilesBox(); + QVBoxLayout *l = new QVBoxLayout; + l->addWidget(fileBox); + setLayout(l); + + setTitle(tr("File options")); +} + +FileOptionsPage::~FileOptionsPage() +{ + +} + +void FileOptionsPage::createFilesBox() +{ + fileBox = new QGroupBox(tr("Files")); + + differentFilesCheckBox = new QCheckBox(tr("Different Files")); + connect(differentFilesCheckBox, &QCheckBox::toggled, + this, &FileOptionsPage::onDifferentFiles); + + pathEdit = new QLineEdit; + connect(pathEdit, &QLineEdit::textChanged, this, &QWizardPage::completeChanged); + + fileBut = new QPushButton(tr("Choose")); + connect(fileBut, &QPushButton::clicked, this, &FileOptionsPage::onChooseFile); + + QLabel *label = new QLabel(tr("File(s)")); + + QGridLayout *l = new QGridLayout; + l->addWidget(differentFilesCheckBox, 0, 0, 1, 2); + l->addWidget(label, 1, 0, 1, 2); + l->addWidget(pathEdit, 2, 0); + l->addWidget(fileBut, 2, 1); + fileBox->setLayout(l); +} + +void FileOptionsPage::initializePage() +{ + pathEdit->setText(mWizard->fileOutput); + if(mWizard->type == Print::Svg) + { + //Svg can only be printed in multiple files + differentFilesCheckBox->setChecked(true); + differentFilesCheckBox->setDisabled(true); + } + else + { + differentFilesCheckBox->setChecked(mWizard->differentFiles); + differentFilesCheckBox->setEnabled(true); + } +} + +bool FileOptionsPage::validatePage() +{ + QString path = pathEdit->text(); + if(path.isEmpty()) + { + return false; + } + + mWizard->fileOutput = QDir::fromNativeSeparators(path); + mWizard->differentFiles = differentFilesCheckBox->isChecked(); + + return true; +} + +bool FileOptionsPage::isComplete() const +{ + return !pathEdit->text().isEmpty(); +} + +void FileOptionsPage::onChooseFile() +{ + QString path; + if(differentFilesCheckBox->isChecked()) + { + path = QFileDialog::getExistingDirectory(this, + tr("Choose Folder"), + pathEdit->text()); + + if(path.isEmpty()) //User canceled dialog + return; + } + else + { + QString ext; + QString fullName; + switch (mWizard->type) + { + case Print::Pdf: + ext = QStringLiteral(".pdf"); + fullName = FileFormats::tr(FileFormats::pdfFile); + break; + case Print::Svg: + ext = QStringLiteral(".svg"); + fullName = FileFormats::tr(FileFormats::svgFile); + break; + default: + break; + } + + path = QFileDialog::getSaveFileName(this, + tr("Choose file"), + pathEdit->text(), + fullName); + if(path.isEmpty()) //User canceled dialog + return; + + if(!path.endsWith(ext)) + path.append(ext); + } + + pathEdit->setText(QDir::fromNativeSeparators(path)); +} + +void FileOptionsPage::onDifferentFiles() +{ + //If pathEdit contains a file but user checks 'Different Files' + //We go up to file directory and use that + + QString path = pathEdit->text(); + if(path.isEmpty()) + return; + + QString ext; + switch (mWizard->type) + { + case Print::Pdf: + ext = QStringLiteral(".pdf"); + break; + case Print::Svg: + ext = QStringLiteral(".svg"); + break; + default: + break; + } + + if(differentFilesCheckBox->isChecked()) + { + if(path.endsWith(ext)) + { + path = path.left(path.lastIndexOf('/')); + } + } + else + { + if(!path.endsWith(ext)) + { + if(path.endsWith('/')) + path.append(QStringLiteral("file%1").arg(ext)); + else + path.append(QStringLiteral("/file%1").arg(ext)); + } + } + + pathEdit->setText(path); +} + +int FileOptionsPage::nextId() const +{ + return 3; //Go to ProgressPage +} diff --git a/src/printing/fileoptionspage.h b/src/printing/fileoptionspage.h new file mode 100644 index 0000000..8c1f1e7 --- /dev/null +++ b/src/printing/fileoptionspage.h @@ -0,0 +1,43 @@ +#ifndef PDFOPTIONSPAGE_H +#define PDFOPTIONSPAGE_H + +#include + +class PrintWizard; +class QLineEdit; +class QPushButton; +class QComboBox; +class QGroupBox; +class QCheckBox; + +class FileOptionsPage : public QWizardPage +{ + Q_OBJECT +public: + FileOptionsPage(PrintWizard *w, QWidget *parent = nullptr); + ~FileOptionsPage(); + + void initializePage() override; + bool validatePage() override; + bool isComplete() const override; + int nextId() const override; + +public slots: + void onChooseFile(); + + void onDifferentFiles(); +private: + void createFilesBox(); + +private: + PrintWizard *mWizard; + + QGroupBox *fileBox; + QCheckBox *differentFilesCheckBox; + QLineEdit *pathEdit; + QPushButton *fileBut; + + QComboBox *pageCombo; +}; + +#endif // PDFOPTIONSPAGE_H diff --git a/src/printing/pdfoptionspage.ui b/src/printing/pdfoptionspage.ui new file mode 100644 index 0000000..796020b --- /dev/null +++ b/src/printing/pdfoptionspage.ui @@ -0,0 +1,59 @@ + + + Form + + + + 0 + 0 + 400 + 222 + + + + + + + + + + + 0 + 0 + + + + Files + + + false + + + false + + + + + + + + + Choose + + + + + + + Different Files + + + + + + + + + + + diff --git a/src/printing/printdefs.h b/src/printing/printdefs.h new file mode 100644 index 0000000..f68c131 --- /dev/null +++ b/src/printing/printdefs.h @@ -0,0 +1,14 @@ +#ifndef DEFS_H +#define DEFS_H + +namespace Print { + +enum OutputType { + Native = 0, + Pdf, + Svg +}; + +} + +#endif // DEFS_H diff --git a/src/printing/printeroptionspage.cpp b/src/printing/printeroptionspage.cpp new file mode 100644 index 0000000..76dc44d --- /dev/null +++ b/src/printing/printeroptionspage.cpp @@ -0,0 +1,27 @@ +#include "printeroptionspage.h" +#include "printwizard.h" + +#include +#include + +#include + +PrinterOptionsPage::PrinterOptionsPage(PrintWizard *w, QWidget *parent) : + QWizardPage (parent), + mWizard(w) +{ + optionsBut = new QPushButton(tr("Open settings")); + connect(optionsBut, &QPushButton::clicked, this, &PrinterOptionsPage::onOpenDialog); + + QFormLayout *l = new QFormLayout; + l->addRow(tr("Printer"), optionsBut); + setLayout(l); + + setTitle(tr("Printer options")); +} + +void PrinterOptionsPage::onOpenDialog() +{ + QPrintDialog dlg(mWizard->printer, this); + dlg.exec(); +} diff --git a/src/printing/printeroptionspage.h b/src/printing/printeroptionspage.h new file mode 100644 index 0000000..249caf1 --- /dev/null +++ b/src/printing/printeroptionspage.h @@ -0,0 +1,24 @@ +#ifndef PRINTEROPTIONS_H +#define PRINTEROPTIONS_H + +#include + +class PrintWizard; +class QPushButton; + +class PrinterOptionsPage : public QWizardPage +{ + Q_OBJECT +public: + PrinterOptionsPage(PrintWizard *w, QWidget *parent = nullptr); + +public slots: + void onOpenDialog(); + +private: + PrintWizard *mWizard; + + QPushButton *optionsBut; +}; + +#endif // PRINTEROPTIONS_H diff --git a/src/printing/printwizard.cpp b/src/printing/printwizard.cpp new file mode 100644 index 0000000..93a867f --- /dev/null +++ b/src/printing/printwizard.cpp @@ -0,0 +1,40 @@ +#include "printwizard.h" + +#include "app/session.h" + +#include "selectionpage.h" +#include "fileoptionspage.h" +#include "printeroptionspage.h" +#include "progresspage.h" + +#include + +PrintWizard::PrintWizard(QWidget *parent) : + QWizard (parent), + differentFiles(false), + type(Print::Native) +{ + printer = new QPrinter; + + setPage(0, new SelectionPage(this)); + setPage(1, new FileOptionsPage(this)); + setPage(2, new PrinterOptionsPage(this)); + setPage(3, new ProgressPage(this)); + + setWindowTitle(tr("Print Wizard")); +} + +PrintWizard::~PrintWizard() +{ + delete printer; +} + +void PrintWizard::setOutputType(Print::OutputType out) +{ + type = out; +} + +Print::OutputType PrintWizard::getOutputType() const +{ + return type; +} diff --git a/src/printing/printwizard.h b/src/printing/printwizard.h new file mode 100644 index 0000000..ff13901 --- /dev/null +++ b/src/printing/printwizard.h @@ -0,0 +1,31 @@ +#ifndef PRINTWIZARD_H +#define PRINTWIZARD_H + +#include +#include + +#include "printdefs.h" + +class QPrinter; + +class PrintWizard : public QWizard +{ + Q_OBJECT +public: + + explicit PrintWizard(QWidget *parent = nullptr); + ~PrintWizard(); + + void setOutputType(Print::OutputType out); + Print::OutputType getOutputType() const; + + QPrinter *printer; + QString fileOutput; + bool differentFiles; + + Print::OutputType type; + + QVector m_checks; +}; + +#endif // PRINTWIZARD_H diff --git a/src/printing/printworker.cpp b/src/printing/printworker.cpp new file mode 100644 index 0000000..2754939 --- /dev/null +++ b/src/printing/printworker.cpp @@ -0,0 +1,213 @@ +#include "printworker.h" + +#include + +#include + +#include + +#include + +#include "graph/graphmanager.h" +#include "graph/backgroundhelper.h" + +#include + +#include + +#include "info.h" + +PrintWorker::PrintWorker(QObject *parent) : + QObject(parent), + differentFiles(false), + outType(Print::Native) +{ + +} + +void PrintWorker::setScenes(const Scenes &scenes) +{ + m_scenes = scenes; +} + +void PrintWorker::doWork() +{ + emit progress(0); + + switch (outType) + { + case Print::Native: + { + m_printer->setOutputFormat(QPrinter::NativeFormat); + printNormal(); + break; + } + case Print::Pdf: + { + m_printer->setOutputFormat(QPrinter::PdfFormat); + m_printer->setCreator(AppDisplayName); + m_printer->setDocName(QStringLiteral("Session")); + + if(differentFiles) + { + printPdfMultipleFiles(); + } + else + { + m_printer->setOutputFileName(fileOutput); + printNormal(); + } + break; + } + case Print::Svg: + { + printSvg(); + } + } + + emit finished(); +} + +void PrintWorker::printPdfMultipleFiles() +{ + QPainter painter; + m_printer->setOutputFormat(QPrinter::PdfFormat); + + const QString fmt = QString("%1/%2.pdf").arg(fileOutput); + + const int size = m_scenes.size(); + for(int i = 0; i < size; i++) + { + auto p = m_scenes[i]; + emit description(p.name); + + m_printer->setOutputFileName(fmt.arg(p.name)); + painter.begin(m_printer); + + printScene(&painter, p); + + painter.end(); + + emit progress(i + 1); + } +} + +void PrintWorker::printSvg() +{ + QSvgGenerator svg; + svg.setTitle(QStringLiteral("Railway Line")); + svg.setDescription(QStringLiteral("Generated by %1").arg(AppDisplayName)); //TODO: make constant, it's used also in other places + + QPainter painter; + + const QString fmt = QString("%1/%2.svg").arg(fileOutput); + + const int size = m_scenes.size(); + for(int i = 0; i < size; i++) + { + auto p = m_scenes[i]; + emit description(p.name); + + QRectF r = p.scene->sceneRect(); + svg.setSize(r.size().toSize()); + svg.setViewBox(r); + + svg.setFileName(fmt.arg(p.name)); + painter.begin(&svg); + + printScene(&painter, p); + + painter.end(); + + emit progress(i + 1); + } +} + +void PrintWorker::printNormal() +{ + QPainter painter(m_printer); + const int size = m_scenes.size(); + for(int i = 0; i < size; i++) + { + auto p = m_scenes[i]; + emit description(p.name); + + printScene(&painter, p); + + emit progress(i + 1); + + if(i < size - 1) + m_printer->newPage(); //Don't add a last empty page + } +} + +void PrintWorker::setPrinter(QPrinter *printer) +{ + m_printer = printer; +} + +void PrintWorker::setOutputType(Print::OutputType type) +{ + outType = type; +} + +void PrintWorker::printScene(QPainter *painter, const Scene& s) +{ + //TODO: maybe optimize: scale and draw directly on printer? + QRectF source = s.scene->sceneRect(); + auto realDev = painter->device(); + QRectF target = QRectF(0, 0, realDev->width(), realDev->height()); + + const qreal textHeight = 20; + target.setTop(target.top() + textHeight); + + //Intermidiate painting on temp QPicture then we scale and play picture on printer + QPicture tmpPic; + QPainter p(&tmpPic); + + BackgroundHelper *helper = graphMgr->getBackGround(); + QRectF labelsRect = source; + labelsRect.setHeight(helper->getVertOffset()); + + helper->drawBackgroundLines(&p, source); + s.scene->render(&p); + helper->drawForegroundHours(&p, source, 0); + helper->drawForegroundStationLabels(&p, labelsRect, + 0, s.lineId); + + p.end(); + + qreal xScale = target.width() / source.width(); + qreal yScale = target.height() / source.height(); + xScale = yScale = qMin(xScale, yScale); + + painter->save(); + + QRectF r(QPointF(0, 0), target.topRight()); + QFont f; + f.setPointSize(15); + painter->setFont(f); + + qDebug() << f << s.name << r; + + painter->drawText(r, Qt::AlignCenter, s.name); + + painter->translate(target.topLeft()); + painter->scale(xScale, yScale); + painter->drawPicture(0, 0, tmpPic); + + painter->restore(); +} + +void PrintWorker::setGraphMgr(GraphManager *value) +{ + graphMgr = value; +} + +void PrintWorker::setFileOutput(const QString &value, bool different) +{ + fileOutput = value; + if(fileOutput.endsWith('/')) + fileOutput.chop(1); + differentFiles = different; +} diff --git a/src/printing/printworker.h b/src/printing/printworker.h new file mode 100644 index 0000000..4cede8d --- /dev/null +++ b/src/printing/printworker.h @@ -0,0 +1,70 @@ +#ifndef PRINTWORKER_H +#define PRINTWORKER_H + +#include + +#include + +#include "printdefs.h" + +#include "utils/types.h" + +class QPrinter; +class QPainter; +class QGraphicsScene; +class GraphManager; + +class PrintWorker : public QObject +{ + Q_OBJECT +public: + + typedef struct Scene_ + { + db_id lineId; + QGraphicsScene *scene; + QString name; + } Scene; + + typedef QVector Scenes; + + explicit PrintWorker(QObject *parent = nullptr); + + void setScenes(const Scenes &scenes); + + void setBackground(QGraphicsScene *background); + void setGraphMgr(GraphManager *value); + + void setPrinter(QPrinter *printer); + + inline int getMaxProgress() { return m_scenes.size(); } + + void setOutputType(Print::OutputType type); + void setFileOutput(const QString &value, bool different); + + void printPdfMultipleFiles(); + void printSvg(); + void printNormal(); + +signals: + void progress(int val); + void description(const QString& text); + void finished(); + +public slots: + void doWork(); + +private: + void printScene(QPainter *painter, const Scene &s); + +private: + Scenes m_scenes; + QPrinter *m_printer; + GraphManager *graphMgr; + + QString fileOutput; + bool differentFiles; + Print::OutputType outType; +}; + +#endif // PRINTWORKER_H diff --git a/src/printing/progresspage.cpp b/src/printing/progresspage.cpp new file mode 100644 index 0000000..e84eef8 --- /dev/null +++ b/src/printing/progresspage.cpp @@ -0,0 +1,128 @@ +#include "progresspage.h" +#include "printwizard.h" + +#include +#include +#include + +#include "printworker.h" + +#include "app/session.h" +#include "viewmanager/viewmanager.h" + +#include "graph/graphmanager.h" + +#include "lines/linestorage.h" + +//BIG TODO: deselect jobs before printing because the dashed rectangle of the selection gets printed too!!! + +ProgressPage::ProgressPage(PrintWizard *w, QWidget *parent) : + QWizardPage(parent), + mWizard(w), + complete(false) +{ + m_label = new QLabel(tr("Printing...")); + m_progressBar = new QProgressBar; + m_progressBar->setMinimum(0); + + QVBoxLayout *l = new QVBoxLayout; + l->addWidget(m_label); + l->addWidget(m_progressBar); + setLayout(l); + + setCommitPage(true); + setFinalPage(true); + + setTitle(tr("Printing")); +} + +void ProgressPage::initializePage() +{ + complete = false; + m_progressBar->reset(); + + m_worker = new PrintWorker; + m_worker->setOutputType(mWizard->type); + m_worker->setPrinter(mWizard->printer); + m_worker->setFileOutput(mWizard->fileOutput, mWizard->differentFiles); + + auto grapgMgr = Session->getViewManager()->getGraphMgr(); + m_worker->setGraphMgr(grapgMgr); + + PrintWorker::Scenes scenes; + + //FIXME: allow selection of witch line to print + query q(Session->m_Db, "SELECT id,name FROM lines"); + for(auto line : q) + { + db_id lineId = line.get(0); + QString name = line.get(1); + QGraphicsScene *scene = Session->mLineStorage->sceneForLine(lineId); + scenes.append({lineId, scene, name}); + } + +// OLD CODE +// LinesModel *model = mWizard->linesModel; + +// for(int row = 0; row < size; row++) +// { +// if(vec.at(row)) +// { +// const LineObj *line = model->getLineObjForRow(row); +// if(!line) +// continue; + +// scenes.append({line->lineId, line->scene, line->name}); +// } +// } + + m_worker->setScenes(scenes); + + m_progressBar->setMaximum(m_worker->getMaxProgress()); + + m_worker->moveToThread(&m_thread); + connect(&m_thread, &QThread::finished, m_worker, &QObject::deleteLater); + connect(&m_thread, &QThread::started, m_worker, &PrintWorker::doWork); + connect(m_worker, &PrintWorker::finished, this, &ProgressPage::handleFinished); + connect(m_worker, &PrintWorker::progress, this, &ProgressPage::handleProgress); + connect(m_worker, &PrintWorker::description, this, &ProgressPage::handleDescription); + + m_thread.start(); +} + +bool ProgressPage::validatePage() +{ + if(m_thread.isRunning()) + { + if(!m_thread.wait(2000)) + return false; + } + + return complete; +} + +bool ProgressPage::isComplete() const +{ + return complete; +} + +void ProgressPage::handleFinished() +{ + m_thread.quit(); + m_thread.wait(); + + m_label->setText(tr("Completed")); + + complete = true; + emit completeChanged(); +} + +void ProgressPage::handleProgress(int val) +{ + m_progressBar->setValue(val); +} + +void ProgressPage::handleDescription(const QString& text) +{ + m_label->setText(QStringLiteral("Printing '%1' ...").arg(text)); +} diff --git a/src/printing/progresspage.h b/src/printing/progresspage.h new file mode 100644 index 0000000..875fedc --- /dev/null +++ b/src/printing/progresspage.h @@ -0,0 +1,37 @@ +#ifndef PROGRESSPAGE_H +#define PROGRESSPAGE_H + +#include + +#include + +class PrintWizard; +class PrintWorker; +class QLabel; +class QProgressBar; + +class ProgressPage : public QWizardPage +{ + Q_OBJECT +public: + ProgressPage(PrintWizard *w, QWidget *parent = nullptr); + + void initializePage() override; + bool validatePage() override; + bool isComplete() const override; +public slots: + void handleFinished(); + void handleProgress(int val); + void handleDescription(const QString &text); +private: + PrintWizard *mWizard; + + QLabel *m_label; + QProgressBar *m_progressBar; + + PrintWorker *m_worker; + QThread m_thread; + bool complete; +}; + +#endif // PROGRESSPAGE_H diff --git a/src/printing/selectionpage.cpp b/src/printing/selectionpage.cpp new file mode 100644 index 0000000..8fbd112 --- /dev/null +++ b/src/printing/selectionpage.cpp @@ -0,0 +1,67 @@ +#include "selectionpage.h" +#include "printwizard.h" + +#include +#include +#include + +#include "utils/checkproxymodel.h" + +#include "app/session.h" + +SelectionPage::SelectionPage(PrintWizard *w, QWidget *parent) : + QWizardPage (parent), + mWizard(w) +{ + proxyModel = new CheckProxyModel(this); + connect(proxyModel, &CheckProxyModel::hasCheck, this, &QWizardPage::completeChanged); + + view = new QListView; + view->setModel(proxyModel); + + selectAllBut = new QPushButton(tr("Select All")); + selectNoneBut = new QPushButton(tr("Unselect All")); + + connect(selectAllBut, &QPushButton::clicked, proxyModel, &CheckProxyModel::selectAll); + connect(selectNoneBut, &QPushButton::clicked, proxyModel, &CheckProxyModel::selectNone); + + QGridLayout *l = new QGridLayout; + l->addWidget(selectAllBut, 0, 0); + l->addWidget(selectNoneBut, 0, 1); + l->addWidget(view, 1, 0, 1, 2); + setLayout(l); + + setTitle(tr("Selection page")); + setSubTitle(tr("Select one or more lines to be printed")); +} + +void SelectionPage::initializePage() +{ + proxyModel->setSourceModel(nullptr); +} + +bool SelectionPage::isComplete() const +{ + return proxyModel->hasAtLeastACheck(); +} + +bool SelectionPage::validatePage() +{ + mWizard->m_checks = proxyModel->checks(); + return true; +} + +int SelectionPage::nextId() const +{ + int ret = 0; + switch (mWizard->type) + { + case Print::Native: + ret = 2; + break; + case Print::Pdf: + case Print::Svg: + ret = 1; + } + return ret; +} diff --git a/src/printing/selectionpage.h b/src/printing/selectionpage.h new file mode 100644 index 0000000..a2de9c6 --- /dev/null +++ b/src/printing/selectionpage.h @@ -0,0 +1,32 @@ +#ifndef SELECTIONPAGE_H +#define SELECTIONPAGE_H + +#include + +class PrintWizard; +class QListView; +class QPushButton; +class CheckProxyModel; + +class SelectionPage : public QWizardPage +{ + Q_OBJECT +public: + SelectionPage(PrintWizard *w, QWidget *parent = nullptr); + + void initializePage() override; + bool isComplete() const override; + bool validatePage() override; + int nextId() const override; + +private: + PrintWizard *mWizard; + + QListView *view; + QPushButton *selectAllBut; + QPushButton *selectNoneBut; + + CheckProxyModel *proxyModel; +}; + +#endif // SELECTIONPAGE_H diff --git a/src/rollingstock/CMakeLists.txt b/src/rollingstock/CMakeLists.txt new file mode 100644 index 0000000..8ae639e --- /dev/null +++ b/src/rollingstock/CMakeLists.txt @@ -0,0 +1,28 @@ +add_subdirectory(importer) +add_subdirectory(manager) +add_subdirectory(rs_checker) + +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + rollingstock/rollingstockmatchmodel.h + rollingstock/rollingstocksqlmodel.h + rollingstock/rsmatchmodelfactory.h + rollingstock/rsmodelsmatchmodel.h + rollingstock/rsmodelssqlmodel.h + rollingstock/rsownersmatchmodel.h + rollingstock/rsownerssqlmodel.h + + rollingstock/rollingstockmatchmodel.cpp + rollingstock/rollingstocksqlmodel.cpp + rollingstock/rsmatchmodelfactory.cpp + rollingstock/rsmodelsmatchmodel.cpp + rollingstock/rsmodelssqlmodel.cpp + rollingstock/rsownersmatchmodel.cpp + rollingstock/rsownerssqlmodel.cpp + PARENT_SCOPE +) + +set(TRAINTIMETABLE_UI_FILES + ${TRAINTIMETABLE_UI_FILES} + PARENT_SCOPE +) diff --git a/src/rollingstock/importer/CMakeLists.txt b/src/rollingstock/importer/CMakeLists.txt new file mode 100644 index 0000000..865c592 --- /dev/null +++ b/src/rollingstock/importer/CMakeLists.txt @@ -0,0 +1,13 @@ +add_subdirectory(backends) +add_subdirectory(intefaces) +add_subdirectory(model) +add_subdirectory(pages) + +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + rollingstock/importer/rsimportstrings.h + rollingstock/importer/rsimportwizard.h + + rollingstock/importer/rsimportwizard.cpp + PARENT_SCOPE +) diff --git a/src/rollingstock/importer/backends/CMakeLists.txt b/src/rollingstock/importer/backends/CMakeLists.txt new file mode 100644 index 0000000..f0092ba --- /dev/null +++ b/src/rollingstock/importer/backends/CMakeLists.txt @@ -0,0 +1,19 @@ +add_subdirectory(ods) +add_subdirectory(sqlite) + +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + rollingstock/importer/backends/importmodes.h + rollingstock/importer/backends/importtask.h + rollingstock/importer/backends/ioptionswidget.h + rollingstock/importer/backends/loadprogressevent.h + rollingstock/importer/backends/loadtaskutils.h + rollingstock/importer/backends/optionsmodel.h + + rollingstock/importer/backends/importtask.cpp + rollingstock/importer/backends/ioptionswidget.cpp + rollingstock/importer/backends/loadprogressevent.cpp + rollingstock/importer/backends/loadtaskutils.cpp + rollingstock/importer/backends/optionsmodel.cpp + PARENT_SCOPE +) diff --git a/src/rollingstock/importer/backends/importmodes.h b/src/rollingstock/importer/backends/importmodes.h new file mode 100644 index 0000000..44ae071 --- /dev/null +++ b/src/rollingstock/importer/backends/importmodes.h @@ -0,0 +1,11 @@ +#ifndef IMPORTMODES_H +#define IMPORTMODES_H + +enum RSImportMode +{ + ImportRSOwners = 1, + ImportRSModels = 2, + ImportRSPieces = 4 +}; + +#endif // IMPORTMODES_H diff --git a/src/rollingstock/importer/backends/importtask.cpp b/src/rollingstock/importer/backends/importtask.cpp new file mode 100644 index 0000000..3541ed5 --- /dev/null +++ b/src/rollingstock/importer/backends/importtask.cpp @@ -0,0 +1,171 @@ +#include "importtask.h" + +#include "utils/types.h" + +#include + +#include "loadprogressevent.h" +#include + +ImportTask::ImportTask(sqlite3pp::database &db, QObject *receiver) : + IQuittableTask(receiver), + mDb(db) +{ +} + +void ImportTask::run() +{ + sendEvent(new LoadProgressEvent(this, 0, 4), false); + + //FIXME: do copy in batches like LoadSQLiteTask + //TODO: get only owners used really + sqlite3pp::query q(mDb, "SELECT imp.id,imp.name,imp.new_name FROM imported_rs_owners imp WHERE imp.import=1 AND imp.match_existing_id IS NULL"); + sqlite3pp::command q_create(mDb, "INSERT INTO rs_owners(id, name) VALUES(NULL, ?)"); + sqlite3pp::command q_update(mDb, "UPDATE imported_rs_owners SET match_existing_id=? WHERE id=?"); + + sqlite3_stmt *st = q.stmt(); + sqlite3_stmt *create = q_create.stmt(); + sqlite3_stmt *update = q_update.stmt(); + + sqlite3 *db = mDb.db(); + sqlite3_mutex *m = sqlite3_db_mutex(db); + + int n = 0; + while (q.step() == SQLITE_ROW) + { + if(n % 8 == 0 && wasStopped()) + { + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressAbortedByUser, + LoadProgressEvent::ProgressMaxFinished), + true); + return; + } + + //Get imported owner id + db_id importedOwnerId = sqlite3_column_int64(st, 0); + int len = 2; + + if(sqlite3_column_type(st, 2) == SQLITE_NULL) + { + len = 1; //Get original name instead of the custom one + } + + //Get its name (or new_name) + const char *name = reinterpret_cast(sqlite3_column_text(st, len)); + len = sqlite3_column_bytes(st, len); + + //Create a real owner with this name and get its id + sqlite3_bind_text(create, 1, name, len, SQLITE_STATIC); + sqlite3_mutex_enter(m); + sqlite3_step(create); + db_id existingOwnerId = sqlite3_last_insert_rowid(db); + sqlite3_mutex_leave(m); + sqlite3_reset(create); + + //Set the real owner id in the imported owner record to use it later + sqlite3_bind_int64(update, 1, existingOwnerId); + sqlite3_bind_int64(update, 2, importedOwnerId); + sqlite3_step(update); + sqlite3_reset(update); + + n++; + } + + sendEvent(new LoadProgressEvent(this, 1, 4), false); + + q.prepare("SELECT imp.id,imp.name,imp.suffix,imp.new_name,imp.max_speed,imp.axes,imp.type,imp.sub_type" + " FROM imported_rs_models imp WHERE imp.import=1 AND imp.match_existing_id IS NULL"); + st = q.stmt(); + + q_create.prepare("INSERT INTO rs_models(id,name,suffix,max_speed,axes,type,sub_type) VALUES(NULL,?,?,?,?,?,?)"); //Create invalid engine + create = q_create.stmt(); + + q_update.prepare("UPDATE imported_rs_models SET match_existing_id=? WHERE id=?"); + update = q_update.stmt(); + + n = 0; + while (q.step() == SQLITE_ROW) + { + if(n % 8 == 0 && wasStopped()) + { + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressAbortedByUser, + LoadProgressEvent::ProgressMaxFinished), + true); + return; + } + + //Get imported model id + db_id importedModelId = sqlite3_column_int64(st, 0); + + int len = 3; + if(sqlite3_column_type(st, 3) == SQLITE_NULL) + { + len = 1; //Get original name instead of the custom one ('name' instead of 'new_name' + } + + //Get its name (or new_name) + const char *name = reinterpret_cast(sqlite3_column_text(st, len)); + len = sqlite3_column_bytes(st, len); + sqlite3_bind_text(create, 1, name, len, SQLITE_STATIC); + + //Get its suffix + name = reinterpret_cast(sqlite3_column_text(st, 2)); + len = sqlite3_column_bytes(st, 2); + sqlite3_bind_text(create, 2, name, len, SQLITE_STATIC); + + //Get its max_speed + int val = sqlite3_column_int(st, 4); + sqlite3_bind_int(create, 3, val); + + //Get its axes count + val = sqlite3_column_int(st, 5); + sqlite3_bind_int(create, 4, val); + + //Get its type + val = sqlite3_column_int(st, 6); + sqlite3_bind_int(create, 5, val); + + //Get its sub_type + val = sqlite3_column_int(st, 7); + sqlite3_bind_int(create, 6, val); + + //Create a real model with this name and get its id + sqlite3_mutex_enter(m); + sqlite3_step(create); + db_id existingModelId = sqlite3_last_insert_rowid(db); + sqlite3_mutex_leave(m); + sqlite3_reset(create); + + //Set the real owner id in the imported model record to use it later + sqlite3_bind_int64(update, 1, existingModelId); + sqlite3_bind_int64(update, 2, importedModelId); + sqlite3_step(update); + sqlite3_reset(update); + n++; + } + + if(wasStopped()) + { + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressAbortedByUser, + LoadProgressEvent::ProgressMaxFinished), + true); + return; + }else{ + sendEvent(new LoadProgressEvent(this, 2, 4), false); + } + + //Finally import rollingstock + q_create.prepare("INSERT INTO rs_list(id, model_id, number, owner_id)" + " SELECT NULL, m.match_existing_id, (CASE WHEN imp.new_number IS NULL THEN imp.number ELSE imp.new_number END), o.match_existing_id" + " FROM imported_rs_list imp" + " JOIN imported_rs_models m ON m.id=imp.model_id " + " JOIN imported_rs_owners o ON o.id=imp.owner_id" + " WHERE imp.import=1 AND m.import=1 AND o.import=1"); + + q_create.execute(); //Long running + + sendEvent(new LoadProgressEvent(this, 0, LoadProgressEvent::ProgressMaxFinished), true); +} diff --git a/src/rollingstock/importer/backends/importtask.h b/src/rollingstock/importer/backends/importtask.h new file mode 100644 index 0000000..8b16003 --- /dev/null +++ b/src/rollingstock/importer/backends/importtask.h @@ -0,0 +1,23 @@ +#ifndef IMPORTTASK_H +#define IMPORTTASK_H + +#include "utils/thread/iquittabletask.h" + +class QObject; + +namespace sqlite3pp { +class database; +} + +class ImportTask : public IQuittableTask +{ +public: + ImportTask(sqlite3pp::database &db, QObject *receiver); + + void run() override; + +private: + sqlite3pp::database &mDb; +}; + +#endif // IMPORTTASK_H diff --git a/src/rollingstock/importer/backends/ioptionswidget.cpp b/src/rollingstock/importer/backends/ioptionswidget.cpp new file mode 100644 index 0000000..32ed50a --- /dev/null +++ b/src/rollingstock/importer/backends/ioptionswidget.cpp @@ -0,0 +1,6 @@ +#include "ioptionswidget.h" + +IOptionsWidget::IOptionsWidget(QWidget *parent) : QWidget(parent) +{ + +} diff --git a/src/rollingstock/importer/backends/ioptionswidget.h b/src/rollingstock/importer/backends/ioptionswidget.h new file mode 100644 index 0000000..a61f575 --- /dev/null +++ b/src/rollingstock/importer/backends/ioptionswidget.h @@ -0,0 +1,18 @@ +#ifndef IOPTIONSWIDGET_H +#define IOPTIONSWIDGET_H + +#include + +#include + +class IOptionsWidget : public QWidget +{ + Q_OBJECT +public: + explicit IOptionsWidget(QWidget *parent = nullptr); + + virtual void loadSettings(const QMap &settings) = 0; + virtual void saveSettings(QMap &settings) = 0; +}; + +#endif // IOPTIONSWIDGET_H diff --git a/src/rollingstock/importer/backends/loadprogressevent.cpp b/src/rollingstock/importer/backends/loadprogressevent.cpp new file mode 100644 index 0000000..12e912b --- /dev/null +++ b/src/rollingstock/importer/backends/loadprogressevent.cpp @@ -0,0 +1,10 @@ +#include "loadprogressevent.h" + +LoadProgressEvent::LoadProgressEvent(QRunnable *self, int pr, int m) : + QEvent(_Type), + task(self), + progress(pr), + max(m) +{ + +} diff --git a/src/rollingstock/importer/backends/loadprogressevent.h b/src/rollingstock/importer/backends/loadprogressevent.h new file mode 100644 index 0000000..908aa1c --- /dev/null +++ b/src/rollingstock/importer/backends/loadprogressevent.h @@ -0,0 +1,29 @@ +#ifndef LOADPROGRESSEVENT_H +#define LOADPROGRESSEVENT_H + +#include +#include "utils/worker_event_types.h" + +class QRunnable; + +class LoadProgressEvent : public QEvent +{ +public: + enum + { + ProgressError = -1, + ProgressAbortedByUser = -2, + ProgressMaxFinished = -3 + }; + + static constexpr Type _Type = Type(CustomEvents::RsImportLoadProgress); + + LoadProgressEvent(QRunnable *self, int pr, int m); + +public: + QRunnable *task; + int progress; + int max; +}; + +#endif // LOADPROGRESSEVENT_H diff --git a/src/rollingstock/importer/backends/loadtaskutils.cpp b/src/rollingstock/importer/backends/loadtaskutils.cpp new file mode 100644 index 0000000..6755bc8 --- /dev/null +++ b/src/rollingstock/importer/backends/loadtaskutils.cpp @@ -0,0 +1,10 @@ +#include "loadtaskutils.h" + + +ILoadRSTask::ILoadRSTask(sqlite3pp::database &db, const QString &fileName, QObject *receiver) : + IQuittableTask(receiver), + mDb(db), + mFileName(fileName) +{ + +} diff --git a/src/rollingstock/importer/backends/loadtaskutils.h b/src/rollingstock/importer/backends/loadtaskutils.h new file mode 100644 index 0000000..4c09743 --- /dev/null +++ b/src/rollingstock/importer/backends/loadtaskutils.h @@ -0,0 +1,33 @@ +#ifndef LOADTASKUTILS_H +#define LOADTASKUTILS_H + +#include +#include "utils/thread/iquittabletask.h" +#include "importmodes.h" + +namespace sqlite3pp { +class database; +} + +class ILoadRSTask : public IQuittableTask +{ +public: + ILoadRSTask(sqlite3pp::database &db, const QString& fileName, QObject *receiver); + + inline QString getErrorText() const { return errText; } + +protected: + sqlite3pp::database &mDb; + + QString mFileName; + QString errText; +}; + +class LoadTaskUtils +{ + Q_DECLARE_TR_FUNCTIONS(LoadTaskUtils) +public: + enum { BatchSize = 50 }; +}; + +#endif // LOADTASKUTILS_H diff --git a/src/rollingstock/importer/backends/ods/CMakeLists.txt b/src/rollingstock/importer/backends/ods/CMakeLists.txt new file mode 100644 index 0000000..567b285 --- /dev/null +++ b/src/rollingstock/importer/backends/ods/CMakeLists.txt @@ -0,0 +1,12 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + rollingstock/importer/backends/ods/loadodstask.h + rollingstock/importer/backends/ods/odsimporter.h + rollingstock/importer/backends/ods/odsoptionswidget.h + rollingstock/importer/backends/ods/options.h + + rollingstock/importer/backends/ods/loadodstask.cpp + rollingstock/importer/backends/ods/odsimporter.cpp + rollingstock/importer/backends/ods/odsoptionswidget.cpp + PARENT_SCOPE +) diff --git a/src/rollingstock/importer/backends/ods/loadodstask.cpp b/src/rollingstock/importer/backends/ods/loadodstask.cpp new file mode 100644 index 0000000..e76a4e1 --- /dev/null +++ b/src/rollingstock/importer/backends/ods/loadodstask.cpp @@ -0,0 +1,189 @@ +#include "loadodstask.h" +#include "odsimporter.h" +#include "../loadprogressevent.h" +#include "options.h" + +#include +#include + +#include + +#include + +LoadODSTask::LoadODSTask(const QMap &arguments, + sqlite3pp::database &db, int mode, int defSpeed, RsType defType, + const QString &fileName, QObject *receiver) : + ILoadRSTask(db, fileName, receiver), + importMode(mode), + defaultSpeed(defSpeed), + defaultType(defType) +{ + m_tblFirstRow = arguments.value(odsFirstRowKey, 3).toInt(); + m_tblRSNumberCol = arguments.value(odsNumColKey, 1).toInt(); + m_tblModelNameCol = arguments.value(odsNameColKey, 3).toInt(); +} + +void LoadODSTask::run() +{ + if(wasStopped()) + { + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressAbortedByUser, + LoadProgressEvent::ProgressMaxFinished), + true); + return; + } + + int max = 4; + int progress = 0; + sendEvent(new LoadProgressEvent(this, progress++, max), false); + + int err = 0; + zip_t *zipper = zip_open(mFileName.toUtf8(), ZIP_RDONLY, &err); + + if (!zipper) + { + zip_error_t ziperror; + zip_error_init_with_code(&ziperror, err); + const char *msg = zip_error_strerror(&ziperror); + qDebug() << "Failed to open output file" << mFileName << "Err:" << msg; + + errText = QString::fromUtf8(msg); + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressError, + LoadProgressEvent::ProgressMaxFinished), + true); + return; + } + + //Search for the file of given name + const char *name = "content.xml"; + struct zip_stat st; + zip_stat_init(&st); + if(zip_stat(zipper, name, 0, &st) < 0) + { + const char *msg = zip_strerror(zipper); + errText = QString::fromUtf8(msg); + + //Close archive + zip_close(zipper); + + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressError, + LoadProgressEvent::ProgressMaxFinished), + true); + return; + } + + QTemporaryFile mContentFile; + + if(mContentFile.isOpen()) + mContentFile.close(); + + if(!mContentFile.open() || !mContentFile.resize(qint64(st.size))) + { + errText = mContentFile.errorString(); + //Close archive + zip_close(zipper); + + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressError, + LoadProgressEvent::ProgressMaxFinished), + true); + return; + } + + //Read the compressed file + zip_file *f = zip_fopen(zipper, name, 0); + if(!f) + { + const char *msg = zip_strerror(zipper); + errText = QString::fromUtf8(msg); + //Close archive + zip_close(zipper); + + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressError, + LoadProgressEvent::ProgressMaxFinished), + true); + return; + } + + zip_uint64_t sum = 0; + char buf[4096]; + while (sum != st.size) + { + zip_int64_t len = zip_fread(f, buf, 4096); + if (len < 0) + { + const char *msg = zip_file_strerror(f); + errText = QString::fromUtf8(msg); + //Close file and archive + zip_fclose(f); + zip_close(zipper); + + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressError, + LoadProgressEvent::ProgressMaxFinished), + true); + return; + } + mContentFile.write(buf, len); + sum += zip_uint64_t(len); + } + + //Close file and archive + zip_fclose(f); + zip_close(zipper); + + if(wasStopped()) + { + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressAbortedByUser, + LoadProgressEvent::ProgressMaxFinished), + true); + return; + } + + sendEvent(new LoadProgressEvent(this, progress++, max), false); + + mContentFile.reset(); //Seek to start + + QXmlStreamReader xml(&mContentFile); + + ODSImporter importer(importMode, m_tblFirstRow, m_tblRSNumberCol, m_tblModelNameCol, + defaultSpeed, defaultType, mDb); + if(!importer.loadDocument(xml)) + { + errText = xml.errorString(); + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressError, + LoadProgressEvent::ProgressMaxFinished), + true); + return; + } + + do{ + if(wasStopped()) + { + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressAbortedByUser, + LoadProgressEvent::ProgressMaxFinished), + true); + return; + } + sendEvent(new LoadProgressEvent(this, progress++, max++), false); + } + while (importer.readNextTable(xml)); + + if(xml.hasError()) + { + progress = LoadProgressEvent::ProgressError; + errText = xml.errorString(); + } + + sendEvent(new LoadProgressEvent(this, + progress, + LoadProgressEvent::ProgressMaxFinished), + true); +} diff --git a/src/rollingstock/importer/backends/ods/loadodstask.h b/src/rollingstock/importer/backends/ods/loadodstask.h new file mode 100644 index 0000000..f617c96 --- /dev/null +++ b/src/rollingstock/importer/backends/ods/loadodstask.h @@ -0,0 +1,37 @@ +#ifndef LOADODSTASK_H +#define LOADODSTASK_H + +#include "../loadtaskutils.h" + +#include "utils/types.h" + +/* LoadODSTask + * + * Loads rollingstock pieces/models/owners from an ODS spreadsheet + * Open Document Format V1.2 (*.ods) + * + * Table Characteristics + * 1) tblFirstRow: first non-empty RS row (starting from 1, not 0) DEFAULT: 3 + * 2) tblRSNumberCol: column from which number is extracted (starting from 1, not 0) DEFAULT: 1 + * 3) tblFirstRow: column from which model name is extracted (starting from 1, not 0) DEFAULT: 3 + */ +class LoadODSTask : public ILoadRSTask +{ +public: + LoadODSTask(const QMap& arguments, sqlite3pp::database &db, + int mode, int defSpeed, RsType defType, + const QString& fileName, QObject *receiver); + + void run() override; + +private: + int m_tblFirstRow; //Start from 1 (not 0) + int m_tblRSNumberCol; //Start from 1 (not 0) + int m_tblModelNameCol; //Start from 1 (not 0) + int importMode; + + int defaultSpeed; + RsType defaultType; +}; + +#endif // LOADODSTASK_H diff --git a/src/rollingstock/importer/backends/ods/odsimporter.cpp b/src/rollingstock/importer/backends/ods/odsimporter.cpp new file mode 100644 index 0000000..c176611 --- /dev/null +++ b/src/rollingstock/importer/backends/ods/odsimporter.cpp @@ -0,0 +1,445 @@ +#include "odsimporter.h" + +#include "utils/types.h" + +#include "../importmodes.h" + +#include + +#include + +const QString offns = QStringLiteral("xmlns:office"); +const QString offvers = QStringLiteral("office:version"); +const QString tbl = QStringLiteral("table:table"); +const QString tblname = QStringLiteral("table:name"); +const QString offbody = QStringLiteral("office:body"); + +ODSImporter::ODSImporter(const int mode, const int firstRow, const int numColm, const int nameCol, int defSpeed, RsType defType, sqlite3pp::database &db) : + mDb(db), + q_addOwner(mDb, "INSERT INTO imported_rs_owners(id, name, import, new_name, match_existing_id, sheet_idx)" + " VALUES(NULL, ?, 1, NULL, ?, ?)"), + q_unsetImported(mDb, "UPDATE imported_rs_owners SET import=0 WHERE id=?"), + q_addModel(mDb, "INSERT INTO imported_rs_models(id, name, suffix, import, new_name, match_existing_id, max_speed, axes, type, sub_type)" + " VALUES(NULL, ?, '', 1, NULL, ?, ?, ?, ?, ?)"), + q_addRS(mDb, "INSERT INTO imported_rs_list(id, import, model_id, owner_id, number, new_number)" + " VALUES(NULL, 1, ?, ?, ?, NULL)"), + + q_findOwner(mDb, "SELECT id FROM rs_owners WHERE name=?"), + q_findModel(mDb, "SELECT id FROM rs_models WHERE name=?"), + q_findImportedModel(mDb, "SELECT id FROM imported_rs_models WHERE name=?"), + + tableFirstRow(firstRow), + tableRSNumberCol(numColm), + tableModelNameCol(nameCol), + importMode(mode), + + sheetIdx(0), + row(0), + col(0), + + defaultSpeed(defSpeed), + defaultType(defType) +{ + mutex = sqlite3_db_mutex(mDb.db()); +} + +bool ODSImporter::loadDocument(QXmlStreamReader& xml) +{ + xml.setNamespaceProcessing(false); + sheetIdx = 0; + + if(!xml.readNextStartElement() || xml.qualifiedName() != QLatin1String("office:document-content")) + return false; + + bool offNsFound = false; + bool offVersFound = false; + + QXmlStreamAttributes attrs = xml.attributes(); + + for(int i = 0; i < attrs.size(); i++) + { + const QXmlStreamAttribute& a = attrs[i]; + if(!offVersFound && a.qualifiedName() == offvers) + { + if(a.value().size() < 3 || a.value().at(1) != '.') + { + qWarning() << "WRONG OFFICE VERSION:" << a.value(); + } + int major = a.value().at(0).digitValue(); + int minor = a.value().at(2).digitValue(); + qDebug() << "FOUND VERSION:" << a.value(); + if(major != 1 || minor < 2) + { + qDebug() << "Error: wrong office:version value"; + } + offVersFound = true; + if(offNsFound) + break; + } + else if(!offNsFound && a.qualifiedName() == offns) + { + offNsFound = true; + if(offVersFound) + break; + } + } + + if(!offVersFound || !offNsFound) + return false; + + while (xml.readNextStartElement() && xml.qualifiedName() != offbody) + { + xml.skipCurrentElement(); + } + + if(xml.hasError()) + { + qDebug() << "XML Error:" << xml.error() << xml.errorString(); + return false; + } + + if(!xml.readNextStartElement() || xml.qualifiedName() != QLatin1String("office:spreadsheet")) + return false; + + if(xml.hasError()) + { + return false; + } + + return true; +} + +bool ODSImporter::readNextTable(QXmlStreamReader& xml) +{ + while(xml.readNextStartElement()) + { + if(xml.qualifiedName() != tbl) + { + xml.skipCurrentElement(); + continue; //Skip unknown elements + } + + readTable(xml); + sheetIdx++; + return true; + } + + return false; +} + +void ODSImporter::readTable(QXmlStreamReader &xml) +{ + db_id importedOwnerId = 0; + if(importMode & RSImportMode::ImportRSOwners) + { + QString name; + for(const QXmlStreamAttribute& a : xml.attributes()) + { + if(a.qualifiedName() == tblname) + { + name = a.value().toString().simplified(); + break; + } + } + + //qDebug() << "OWNER:" << name; + + //Try to match an existing owner, if returns 0 -> no match -> create new owner + db_id existingOwnerId = 0; + q_findOwner.bind(1, name); + if(q_findOwner.step() == SQLITE_ROW) + existingOwnerId = q_findOwner.getRows().get(0); + q_findOwner.reset(); + + if(name.isEmpty()) + q_addOwner.bind(1); //Bind NULL, will be handled by SelectOwnersPage + else + q_addOwner.bind(1, name); + + if(existingOwnerId) + q_addOwner.bind(2, existingOwnerId); + else + q_addOwner.bind(2); //bind NULL + + q_addOwner.bind(3, sheetIdx); + + sqlite3_mutex_enter(mutex); + int ret = q_addOwner.execute(); + importedOwnerId = mDb.last_insert_rowid(); + sqlite3_mutex_leave(mutex); + q_addOwner.reset(); + + if(ret != SQLITE_OK) + { + //FIXME: tell the user + qWarning() << "Error importing owner:" << name << "skipping..."; + xml.skipCurrentElement(); + return; + } + } + + row = 0; + col = 0; + + int rsCount = 0; + bool finished = false; + + QByteArray model; + qint64 number; + + while (!finished && xml.readNext() != QXmlStreamReader::Invalid) + { + switch (xml.tokenType()) + { + case QXmlStreamReader::StartElement: + { + if(importMode & RSImportMode::ImportRSModels && xml.qualifiedName() == QLatin1String("table:table-row")) + { + readRow(xml, model, number); + + if(row < tableFirstRow || model.isEmpty() || number == -1) + break; //First n rows are table header / empty + + //qDebug() << "RS:" << model << number; + + db_id importedModelId = 0; + + sqlite3_bind_text(q_findImportedModel.stmt(), 1, model, model.size(), SQLITE_STATIC); + if(q_findImportedModel.step() == SQLITE_ROW) + { + importedModelId = q_findImportedModel.getRows().get(0); + } + q_findImportedModel.reset(); + + if(!importedModelId) + { + //Create new one + db_id existingModelId = 0; + int maxSpeedKm = defaultSpeed; + int axes = 4; + RsType type = defaultType; + RsEngineSubType subType = RsEngineSubType::Invalid; + + //Try filling with matched name model infos + sqlite3_bind_text(q_findModel.stmt(), 1, model, model.size(), SQLITE_STATIC); + if(q_findModel.step() == SQLITE_ROW) + { + auto m = q_findModel.getRows(); + existingModelId = m.get(0); + maxSpeedKm = m.get(1); + axes = m.get(2); + type = RsType(m.get(3)); + subType = RsEngineSubType(m.get(4)); + } + q_findModel.reset(); + + sqlite3_bind_text(q_addModel.stmt(), 1, model, model.size(), SQLITE_STATIC); + if(existingModelId) + q_addModel.bind(2, existingModelId); + else + q_addModel.bind(2); //bind NULL + q_addModel.bind(3, maxSpeedKm); + q_addModel.bind(4, axes); + q_addModel.bind(5, int(type)); + q_addModel.bind(6, int(subType)); + + sqlite3_mutex_enter(mutex); + q_addModel.execute(); + importedModelId = mDb.last_insert_rowid(); + sqlite3_mutex_leave(mutex); + + q_addModel.reset(); + } + + if(importMode & RSImportMode::ImportRSPieces) + { + int num = number % 10000; //Cut at 4 digits + + //Finally register RS + q_addRS.bind(1, importedModelId); + q_addRS.bind(2, importedOwnerId); + q_addRS.bind(3, num); + q_addRS.execute(); + q_addRS.reset(); + + rsCount++; + } + } + else + { + xml.skipCurrentElement(); + } + break; + } + case QXmlStreamReader::EndElement: + { + finished = true; + break; + } + default: + break; + } + } + + if(rsCount == 0 && importMode & RSImportMode::ImportRSPieces) + { + q_unsetImported.bind(1, importedOwnerId); + q_unsetImported.execute(); + q_unsetImported.reset(); + } +} + +void ODSImporter::readRow(QXmlStreamReader& xml, QByteArray& model, qint64& number) +{ + row++; + col = 0; + + for(const QXmlStreamAttribute& a : xml.attributes()) + { + if(a.qualifiedName() == QLatin1String("table:number-rows-repeated")) + { + int rowsRepeated = a.value().toInt(); + row += rowsRepeated - 1; + break; + } + } + + while (xml.readNext() != QXmlStreamReader::Invalid) + { + switch (xml.tokenType()) + { + case QXmlStreamReader::StartElement: + { + if(xml.qualifiedName() == QLatin1String("table:table-cell")) + { + int oldCol = col; + + col++; + for(const QXmlStreamAttribute& a : xml.attributes()) + { + if(a.qualifiedName() == QLatin1String("table:number-columns-repeated")) + { + int colsRepeated = a.value().toInt(); + col += colsRepeated - 1; + break; + } + } + + //Read current cell + int depth = 1; + bool cellEmpty = true; + QXmlStreamReader::TokenType token = QXmlStreamReader::NoToken; + while (depth && (token = xml.readNext()) != QXmlStreamReader::Invalid) + { + switch (token) + { + case QXmlStreamReader::StartElement: + depth++; + break; + case QXmlStreamReader::EndElement: + depth--; + break; + case QXmlStreamReader::Characters: + { + if(xml.isWhitespace()) + break; + + //Convert to QString immidiately because xml reader changes buffer contents when reading next token + QStringRef val = xml.text(); + cellEmpty = val.isEmpty(); + //qDebug() << "CELL:" << row << col << val; + + //Avoid allocating a QString copy of QStringRef, directly convert to QByteArray + if(oldCol < tableRSNumberCol && col >= tableRSNumberCol && val.size()) + { + //Do not use toInt(), we must tolerate dashes and other non-digit characters in the middle + qint64 tmp = 0; + for(int i = 0; i < val.size(); i++) + { + int d = val.at(i).digitValue(); + if(d != -1) //-1 means it's not a digit so skip it + { + tmp *= 10; + tmp += d; + } + } + number = tmp; + } + + if(oldCol < tableModelNameCol && col >= tableModelNameCol) + model = val.toUtf8().simplified(); + + break; + } + default: + break; + } + } + + if(cellEmpty) + { + if(oldCol < tableRSNumberCol && col >= tableRSNumberCol) + number = -1; + if(oldCol < tableModelNameCol && col >= tableModelNameCol) + model.clear(); + } + + } + else + { + xml.skipCurrentElement(); + } + break; + } + case QXmlStreamReader::EndElement: + return; + default: + break; + } + } +} + +QString ODSImporter::readCell(QXmlStreamReader &xml) +{ + col++; + for(const QXmlStreamAttribute& a : xml.attributes()) + { + if(a.qualifiedName() == QLatin1String("table:number-columns-repeated")) + { + int colsRepeated = a.value().toInt(); + col += colsRepeated - 1; + break; + } + } + + QString val; + + //Read current cell + int depth = 1; + QXmlStreamReader::TokenType token = QXmlStreamReader::NoToken; + while (depth && (token = xml.readNext()) != QXmlStreamReader::Invalid) + { + switch (token) + { + case QXmlStreamReader::StartElement: + depth++; + break; + case QXmlStreamReader::EndElement: + depth--; + break; + case QXmlStreamReader::Characters: + { + if(xml.isWhitespace()) + break; + + val = xml.text().toString(); //Convert to QString immidiately because xml reader changes buffer contents when reading next token + //qDebug() << "CELL:" << row << col << val; + break; + } + default: + break; + } + } + + return val; +} diff --git a/src/rollingstock/importer/backends/ods/odsimporter.h b/src/rollingstock/importer/backends/ods/odsimporter.h new file mode 100644 index 0000000..8845770 --- /dev/null +++ b/src/rollingstock/importer/backends/ods/odsimporter.h @@ -0,0 +1,50 @@ +#ifndef ODSIMPORTER_H +#define ODSIMPORTER_H + +#include + +#include "utils/types.h" + +class QXmlStreamReader; +typedef struct sqlite3_mutex sqlite3_mutex; + +class ODSImporter +{ +public: + ODSImporter(const int mode, const int firstRow, const int numColm, const int nameCol, + int defSpeed, RsType defType, sqlite3pp::database &db); + + bool loadDocument(QXmlStreamReader &xml); + bool readNextTable(QXmlStreamReader &xml); + +private: + void readTable(QXmlStreamReader& xml); + void readRow(QXmlStreamReader &xml, QByteArray &model, qint64 &number); + QString readCell(QXmlStreamReader &xml); + +private: + sqlite3pp::database &mDb; + sqlite3_mutex *mutex; + sqlite3pp::command q_addOwner; + sqlite3pp::command q_unsetImported; + sqlite3pp::command q_addModel; + sqlite3pp::command q_addRS; + + sqlite3pp::query q_findOwner; + sqlite3pp::query q_findModel; + sqlite3pp::query q_findImportedModel; + + const int tableFirstRow; //Start from 1 (not 0) + const int tableRSNumberCol; //Start from 1 (not 0) + const int tableModelNameCol; //Start from 1 (not 0) + const int importMode; + + int sheetIdx; + int row; + int col; + + int defaultSpeed; + RsType defaultType; +}; + +#endif // ODSIMPORTER_H diff --git a/src/rollingstock/importer/backends/ods/odsoptionswidget.cpp b/src/rollingstock/importer/backends/ods/odsoptionswidget.cpp new file mode 100644 index 0000000..bba0318 --- /dev/null +++ b/src/rollingstock/importer/backends/ods/odsoptionswidget.cpp @@ -0,0 +1,42 @@ +#include "odsoptionswidget.h" +#include "options.h" + +#include +#include +#include + +#include "app/session.h" + +ODSOptionsWidget::ODSOptionsWidget(QWidget *parent) : + IOptionsWidget(parent) +{ + //ODS Option + QFormLayout *lay = new QFormLayout(this); + lay->addRow(new QLabel(tr("Import rollingstock pieces, models and owners from a spreadsheet file.\n" + "The file must be a valid Open Document Format Spreadsheet V1.2\n" + "Extension: (*.ods)"))); + odsFirstRowSpin = new QSpinBox; + odsFirstRowSpin->setRange(1, 9999); + lay->addRow(tr("First non-empty row that contains rollingstock piece information"), odsFirstRowSpin); + odsNumColSpin = new QSpinBox; + odsNumColSpin->setRange(1, 9999); + lay->addRow(tr("Column from which item number is extracted"), odsNumColSpin); + odsNameColSpin = new QSpinBox; + odsNameColSpin->setRange(1, 9999); + lay->addRow(tr("Column from which item model name is extracted"), odsNameColSpin); + lay->setAlignment(Qt::AlignTop | Qt::AlignRight); +} + +void ODSOptionsWidget::loadSettings(const QMap &settings) +{ + odsFirstRowSpin->setValue(settings.value(odsFirstRowKey, AppSettings.getODSFirstRow()).toInt()); + odsNumColSpin->setValue(settings.value(odsNumColKey, AppSettings.getODSNumCol()).toInt()); + odsNameColSpin->setValue(settings.value(odsNameColKey, AppSettings.getODSNameCol()).toInt()); +} + +void ODSOptionsWidget::saveSettings(QMap &settings) +{ + settings.insert(odsFirstRowKey, odsFirstRowSpin->value()); + settings.insert(odsNumColKey, odsNumColSpin->value()); + settings.insert(odsNameColKey, odsNameColSpin->value()); +} diff --git a/src/rollingstock/importer/backends/ods/odsoptionswidget.h b/src/rollingstock/importer/backends/ods/odsoptionswidget.h new file mode 100644 index 0000000..a17492c --- /dev/null +++ b/src/rollingstock/importer/backends/ods/odsoptionswidget.h @@ -0,0 +1,23 @@ +#ifndef ODSOPTIONSWIDGET_H +#define ODSOPTIONSWIDGET_H + +#include "../ioptionswidget.h" + +class QSpinBox; + +class ODSOptionsWidget : public IOptionsWidget +{ + Q_OBJECT +public: + explicit ODSOptionsWidget(QWidget *parent = nullptr); + + void loadSettings(const QMap &settings) override; + void saveSettings(QMap &settings) override; + +private: + QSpinBox *odsFirstRowSpin; + QSpinBox *odsNumColSpin; + QSpinBox *odsNameColSpin; +}; + +#endif // ODSOPTIONSWIDGET_H diff --git a/src/rollingstock/importer/backends/ods/options.h b/src/rollingstock/importer/backends/ods/options.h new file mode 100644 index 0000000..706f278 --- /dev/null +++ b/src/rollingstock/importer/backends/ods/options.h @@ -0,0 +1,8 @@ +#ifndef OPTIONS_H +#define OPTIONS_H + +constexpr const char *odsFirstRowKey = "odsFirstRow"; +constexpr const char *odsNumColKey = "odsNumCol"; +constexpr const char *odsNameColKey = "odsNameCol"; + +#endif // OPTIONS_H diff --git a/src/rollingstock/importer/backends/optionsmodel.cpp b/src/rollingstock/importer/backends/optionsmodel.cpp new file mode 100644 index 0000000..cab135b --- /dev/null +++ b/src/rollingstock/importer/backends/optionsmodel.cpp @@ -0,0 +1,32 @@ +#include "optionsmodel.h" + +#include "utils/file_format_names.h" + +OptionsModel::OptionsModel(QObject *parent) : + QAbstractListModel(parent) +{ + +} + +int OptionsModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : int(RSImportWizard::ImportSource::NSources); +} + +QVariant OptionsModel::data(const QModelIndex &idx, int role) const +{ + if(role != Qt::DisplayRole || idx.row() < 0 || idx.row() >= int(RSImportWizard::ImportSource::NSources) || idx.column() != 0) + return QVariant(); + + RSImportWizard::ImportSource source = RSImportWizard::ImportSource(idx.row()); + switch (source) + { + case RSImportWizard::ImportSource::OdsImport: + return FileFormats::tr(FileFormats::odsFormat); + case RSImportWizard::ImportSource::SQLiteImport: + return FileFormats::tr(FileFormats::tttFormat); + default: + break; + } + return QVariant(); +} diff --git a/src/rollingstock/importer/backends/optionsmodel.h b/src/rollingstock/importer/backends/optionsmodel.h new file mode 100644 index 0000000..5f64cf8 --- /dev/null +++ b/src/rollingstock/importer/backends/optionsmodel.h @@ -0,0 +1,16 @@ +#ifndef OPTIONSMODEL_H +#define OPTIONSMODEL_H + +#include +#include "../rsimportwizard.h" + +class OptionsModel : public QAbstractListModel +{ +public: + OptionsModel(QObject *parent); + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const; + virtual QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const; +}; + +#endif // OPTIONSMODEL_H diff --git a/src/rollingstock/importer/backends/sqlite/CMakeLists.txt b/src/rollingstock/importer/backends/sqlite/CMakeLists.txt new file mode 100644 index 0000000..b8b9ead --- /dev/null +++ b/src/rollingstock/importer/backends/sqlite/CMakeLists.txt @@ -0,0 +1,9 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + rollingstock/importer/backends/sqlite/loadsqlitetask.h + rollingstock/importer/backends/sqlite/sqliteoptionswidget.h + + rollingstock/importer/backends/sqlite/loadsqlitetask.cpp + rollingstock/importer/backends/sqlite/sqliteoptionswidget.cpp + PARENT_SCOPE +) diff --git a/src/rollingstock/importer/backends/sqlite/loadsqlitetask.cpp b/src/rollingstock/importer/backends/sqlite/loadsqlitetask.cpp new file mode 100644 index 0000000..481ffb1 --- /dev/null +++ b/src/rollingstock/importer/backends/sqlite/loadsqlitetask.cpp @@ -0,0 +1,400 @@ +#include "loadsqlitetask.h" +#include "../loadprogressevent.h" + +#include "../loadtaskutils.h" + +#include + +#include + +LoadSQLiteTask::LoadSQLiteTask(sqlite3pp::database &db, int mode, const QString &fileName, QObject *receiver) : + ILoadRSTask(db, fileName, receiver), + importMode(mode) +{ + +} + +void LoadSQLiteTask::run() +{ + currentProgress = 0; + localCount = 0; + localProgress = 0; + + if(wasStopped()) + { + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressAbortedByUser, + LoadProgressEvent::ProgressMaxFinished), + true); + return; + } + + sendEvent(new LoadProgressEvent(this, 0, MaxProgress), false); + + if(!attachDB()) + return; + + if(!copyOwners()) + return; + + if(!copyModels()) + return; + + if(!copyRS()) + return; + + //Cleanup + mDb.execute("DETACH rs_source"); + + if(!unselectOwnersWithNoRS()) + return; + + sendEvent(new LoadProgressEvent(this, + MaxProgress, + LoadProgressEvent::ProgressMaxFinished), + true); +} + +void LoadSQLiteTask::endWithDbError(const QString &text) +{ + mDb.execute("DETACH rs_source"); + + errText = LoadTaskUtils::tr("%1\n" + "Code: %2\n" + "Message: %3") + .arg(text) + .arg(mDb.extended_error_code()) + .arg(mDb.error_msg()); + + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressError, + LoadProgressEvent::ProgressMaxFinished), + true); +} + +bool LoadSQLiteTask::attachDB() +{ + //ATTACH other database to this session + localCount = 1; + sqlite3pp::command q(mDb, "ATTACH ? AS rs_source"); //TODO: use URI to pass 'readonly' + q.bind(1, mFileName); + int ret = q.execute(); + if(ret != SQLITE_OK) + { + endWithDbError(LoadTaskUtils::tr("Could not open session file correctly.")); + return false; + } + + localProgress = 0; //Advance by 1 step, clear partial progress + currentProgress += StepSize; + sendEvent(new LoadProgressEvent(this, calcProgress(), MaxProgress), false); + return true; +} + +bool LoadSQLiteTask::copyOwners() +{ + if((importMode & RSImportMode::ImportRSOwners) == 0) + return true; //Skip owners importation + + sqlite3pp::query q(mDb); + sqlite3pp::query q_getFirstIdGreaterThan(mDb); + + //Calculate maximum number of batches + q.prepare("SELECT MAX(id) FROM rs_source.rs_owners"); + q.step(); + localCount = sqlite3_column_int(q.stmt(), 0); + localCount = localCount/LoadTaskUtils::BatchSize + (localCount % LoadTaskUtils::BatchSize != 0); //Round up + if(localCount < 1) + localCount = 1; //At least 1 batch + + //Init query: get first id to import + int ret = q_getFirstIdGreaterThan.prepare("SELECT MIN(id) FROM rs_source.rs_owners WHERE id>?"); + sqlite3_stmt *stmt = q_getFirstIdGreaterThan.stmt(); + if(ret != SQLITE_OK) + { + endWithDbError(LoadTaskUtils::tr("Query preparation failed A1.")); + return false; + } + + ret = q.prepare("INSERT OR IGNORE INTO main.imported_rs_owners(id, name, import, new_name, match_existing_id, sheet_idx)" + " SELECT NULL,own1.name,1,NULL,own2.id,0" + " FROM rs_source.rs_owners AS own1" + " LEFT JOIN main.rs_owners own2 ON own1.name=own2.name" + " WHERE own1.id BETWEEN ? AND ?"); + if(ret != SQLITE_OK) + { + endWithDbError(LoadTaskUtils::tr("Query preparation failed A2.")); + return false; + } + + int firstId = 0; + localProgress = 0; + while (true) + { + if(wasStopped()) + { + q.prepare("DETACH rs_source"); //Cleanup + q.step(); + + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressAbortedByUser, + LoadProgressEvent::ProgressMaxFinished), + true); + return false; + } + + q_getFirstIdGreaterThan.bind(1, firstId); + q_getFirstIdGreaterThan.step(); + if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) + break; //No more owners to import + + firstId = sqlite3_column_int(stmt, 0); + q_getFirstIdGreaterThan.reset(); + + const int lastId = firstId + LoadTaskUtils::BatchSize; + q.bind(1, firstId); + q.bind(2, lastId); + firstId = lastId; + + q.step(); + + localProgress++; + sendEvent(new LoadProgressEvent(this, calcProgress(), MaxProgress), false); + } + + localProgress = 0; //Advance by 1 step, clear partial progress + currentProgress += StepSize; + sendEvent(new LoadProgressEvent(this, calcProgress(), MaxProgress), false); + return true; +} + +bool LoadSQLiteTask::copyModels() +{ + if((importMode & RSImportMode::ImportRSModels) == 0) + return true; //Skip models importation + + sqlite3pp::query q(mDb); + sqlite3pp::query q_getFirstIdGreaterThan(mDb); + + //Calculate maximum number of batches + q.prepare("SELECT MAX(id) FROM rs_source.rs_models"); + q.step(); + localCount = sqlite3_column_int(q.stmt(), 0); + localCount = localCount/LoadTaskUtils::BatchSize + (localCount % LoadTaskUtils::BatchSize != 0); //Round up + if(localCount < 1) + localCount = 1; //At least 1 batch + + //Init query: get first id to import + int ret = q_getFirstIdGreaterThan.prepare("SELECT MIN(id) FROM rs_source.rs_models WHERE id>?"); + sqlite3_stmt *stmt = q_getFirstIdGreaterThan.stmt(); + if(ret != SQLITE_OK) + { + endWithDbError(LoadTaskUtils::tr("Query preparation failed B1.")); + return false; + } + + ret = q.prepare("INSERT OR IGNORE INTO main.imported_rs_models(id, name, suffix, import, new_name, match_existing_id, max_speed, axes, type, sub_type)" + " SELECT NULL,mod1.name,mod1.suffix,1,NULL,mod2.id,mod1.max_speed,mod1.axes,mod1.type,mod1.sub_type" + " FROM rs_source.rs_models AS mod1" + " LEFT JOIN main.rs_models mod2 ON mod1.name=mod2.name AND mod1.suffix=mod2.suffix" + " WHERE mod1.id BETWEEN ? AND ?"); + if(ret != SQLITE_OK) + { + endWithDbError(LoadTaskUtils::tr("Query preparation failed B2.")); + return false; + } + + int firstId = 0; + localProgress = 0; + while (true) + { + if(wasStopped()) + { + q.prepare("DETACH rs_source"); //Cleanup + q.step(); + + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressAbortedByUser, + LoadProgressEvent::ProgressMaxFinished), + true); + return false; + } + + q_getFirstIdGreaterThan.bind(1, firstId); + q_getFirstIdGreaterThan.step(); + if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) + break; //No more models to import + + firstId = sqlite3_column_int(stmt, 0); + q_getFirstIdGreaterThan.reset(); + + const int lastId = firstId + LoadTaskUtils::BatchSize; + q.bind(1, firstId); + q.bind(2, lastId); + firstId = lastId; + + q.step(); + + localProgress++; + sendEvent(new LoadProgressEvent(this, calcProgress(), MaxProgress), false); + } + + localProgress = 0; //Advance by 1 step, clear partial progress + currentProgress += StepSize; + sendEvent(new LoadProgressEvent(this, calcProgress(), MaxProgress), false); + return true; +} + +bool LoadSQLiteTask::copyRS() +{ + if((importMode & RSImportMode::ImportRSPieces) == 0) + return true; //Skip RS importation + + sqlite3pp::query q(mDb); + sqlite3pp::query q_getFirstIdGreaterThan(mDb); + + //Calculate maximum number of batches + q.prepare("SELECT MAX(id) FROM rs_source.rs_models"); + q.step(); + localCount = sqlite3_column_int(q.stmt(), 0); + localCount = localCount/LoadTaskUtils::BatchSize + (localCount % LoadTaskUtils::BatchSize != 0); //Round up + if(localCount < 1) + localCount = 1; //At least 1 batch + + //Init query: get first id to import + int ret = q_getFirstIdGreaterThan.prepare("SELECT MIN(id) FROM rs_source.rs_models WHERE id>?"); + sqlite3_stmt *stmt = q_getFirstIdGreaterThan.stmt(); + if(ret != SQLITE_OK) + { + endWithDbError(LoadTaskUtils::tr("Query preparation failed C1.")); + return false; + } + + ret = q.prepare("INSERT OR IGNORE INTO main.imported_rs_list(id, import, model_id, owner_id, number, new_number)" + " SELECT NULL,1,mod2.id,own2.id,rs.number % 10000,NULL" + " FROM rs_source.rs_list AS rs" + " LEFT JOIN rs_source.rs_models mod1 ON mod1.id=rs.model_id" + " LEFT JOIN main.imported_rs_models mod2 ON mod1.name=mod2.name AND mod1.suffix=mod2.suffix" + " LEFT JOIN rs_source.rs_owners own1 ON own1.id=rs.owner_id" + " LEFT JOIN main.imported_rs_owners own2 ON own1.name=own2.name" + " WHERE mod1.id BETWEEN ? AND ?"); + if(ret != SQLITE_OK) + { + endWithDbError(LoadTaskUtils::tr("Query preparation failed C2.")); + return false; + } + + int firstId = 0; + localProgress = 0; + while (true) + { + if(wasStopped()) + { + q.prepare("DETACH rs_source"); //Cleanup + q.step(); + + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressAbortedByUser, + LoadProgressEvent::ProgressMaxFinished), + true); + return false; + } + + q_getFirstIdGreaterThan.bind(1, firstId); + q_getFirstIdGreaterThan.step(); + if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) + break; //No more RS to import + + firstId = sqlite3_column_int(stmt, 0); + q_getFirstIdGreaterThan.reset(); + + const int lastId = firstId + LoadTaskUtils::BatchSize; + q.bind(1, firstId); + q.bind(2, lastId); + firstId = lastId; + + q.step(); + + localProgress++; + sendEvent(new LoadProgressEvent(this, calcProgress(), MaxProgress), false); + } + + localProgress = 0; //Advance by 1 step, clear partial progress + currentProgress += StepSize; + sendEvent(new LoadProgressEvent(this, calcProgress(), MaxProgress), false); + return true; +} + +bool LoadSQLiteTask::unselectOwnersWithNoRS() +{ + sqlite3pp::query q(mDb); + sqlite3pp::query q_getFirstIdGreaterThan(mDb); + + //Calculate maximum number of batches + q.prepare("SELECT MAX(id) FROM imported_rs_owners"); + q.step(); + localCount = sqlite3_column_int(q.stmt(), 0); + localCount = localCount/LoadTaskUtils::BatchSize + (localCount % LoadTaskUtils::BatchSize != 0); //Round up + if(localCount < 1) + localCount = 1; //At least 1 batch + + //Init query: get first id to query + int ret = q_getFirstIdGreaterThan.prepare("SELECT MIN(id) FROM imported_rs_owners WHERE id>?"); + sqlite3_stmt *stmt = q_getFirstIdGreaterThan.stmt(); + if(ret != SQLITE_OK) + { + endWithDbError(LoadTaskUtils::tr("Query preparation failed C1.")); + return false; + } + + ret = q.prepare("UPDATE imported_rs_owners SET import=0 WHERE id IN(" + " SELECT own.id" + " FROM imported_rs_owners own" + " LEFT JOIN imported_rs_list rs ON rs.owner_id=own.id" + " WHERE own.id BETWEEN ? AND ?" + " GROUP BY own.id" + " HAVING COUNT(rs.id)=0)"); + if(ret != SQLITE_OK) + { + endWithDbError(LoadTaskUtils::tr("Query preparation failed C2.")); + return false; + } + + int firstId = 0; + localProgress = 0; + while (true) + { + if(wasStopped()) + { + //No cleanup, already done after importing RS + sendEvent(new LoadProgressEvent(this, + LoadProgressEvent::ProgressAbortedByUser, + LoadProgressEvent::ProgressMaxFinished), + true); + return false; + } + + q_getFirstIdGreaterThan.bind(1, firstId); + q_getFirstIdGreaterThan.step(); + if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) + break; //No more owners to edit + + firstId = sqlite3_column_int(stmt, 0); + q_getFirstIdGreaterThan.reset(); + + const int lastId = firstId + LoadTaskUtils::BatchSize; + q.bind(1, firstId); + q.bind(2, lastId); + firstId = lastId; + + q.step(); + + localProgress++; + sendEvent(new LoadProgressEvent(this, calcProgress(), MaxProgress), false); + } + + localProgress = 0; //Advance by 1 step, clear partial progress + currentProgress += StepSize; + sendEvent(new LoadProgressEvent(this, calcProgress(), MaxProgress), false); + return true; +} diff --git a/src/rollingstock/importer/backends/sqlite/loadsqlitetask.h b/src/rollingstock/importer/backends/sqlite/loadsqlitetask.h new file mode 100644 index 0000000..29ce233 --- /dev/null +++ b/src/rollingstock/importer/backends/sqlite/loadsqlitetask.h @@ -0,0 +1,40 @@ +#ifndef LOADSQLITETASK_H +#define LOADSQLITETASK_H + +#include "../loadtaskutils.h" + +/* LoadSQLiteTask + * + * Loads rollingstock pieces/models/owners from + * another TrainTimetable Session (*ttt, *.db, other SQLite extensions) + */ +class LoadSQLiteTask : public ILoadRSTask +{ +public: + //5 steps + //Load DB, Load Owners, Load Models, Load RS, Unselect Owners + enum { StepSize = 100, MaxProgress = 5 * StepSize }; + + LoadSQLiteTask(sqlite3pp::database &db, int mode, const QString& fileName, QObject *receiver); + + void run() override; + +private: + void endWithDbError(const QString& text); + + inline int calcProgress() const { return currentProgress + localProgress / localCount * StepSize; } + + bool attachDB(); + bool copyOwners(); + bool copyModels(); + bool copyRS(); + bool unselectOwnersWithNoRS(); + +private: + const int importMode; + int currentProgress; + int localCount; + int localProgress; +}; + +#endif // LOADSQLITETASK_H diff --git a/src/rollingstock/importer/backends/sqlite/sqliteoptionswidget.cpp b/src/rollingstock/importer/backends/sqlite/sqliteoptionswidget.cpp new file mode 100644 index 0000000..443fed5 --- /dev/null +++ b/src/rollingstock/importer/backends/sqlite/sqliteoptionswidget.cpp @@ -0,0 +1,25 @@ +#include "sqliteoptionswidget.h" + +#include +#include + +SQLiteOptionsWidget::SQLiteOptionsWidget(QWidget *parent) : + IOptionsWidget(parent) +{ + //SQLite Option + QVBoxLayout *lay = new QVBoxLayout(this); + lay->addWidget(new QLabel(tr("Import rollingstock pieces, models and owners from another Train Timetable session file.\n" + "The file must be a valid Train Timetable session of recent version\n" + "Extension: (*.ttt)"))); + lay->setAlignment(Qt::AlignTop | Qt::AlignRight); +} + +void SQLiteOptionsWidget::loadSettings(const QMap &/*settings*/) +{ + +} + +void SQLiteOptionsWidget::saveSettings(QMap &/*settings*/) +{ + +} diff --git a/src/rollingstock/importer/backends/sqlite/sqliteoptionswidget.h b/src/rollingstock/importer/backends/sqlite/sqliteoptionswidget.h new file mode 100644 index 0000000..fda9978 --- /dev/null +++ b/src/rollingstock/importer/backends/sqlite/sqliteoptionswidget.h @@ -0,0 +1,16 @@ +#ifndef SQLITEOPTIONSWIDGET_H +#define SQLITEOPTIONSWIDGET_H + +#include "../ioptionswidget.h" + +class SQLiteOptionsWidget : public IOptionsWidget +{ + Q_OBJECT +public: + SQLiteOptionsWidget(QWidget *parent); + + void loadSettings(const QMap &settings) override; + void saveSettings(QMap &settings) override; +}; + +#endif // SQLITEOPTIONSWIDGET_H diff --git a/src/rollingstock/importer/intefaces/CMakeLists.txt b/src/rollingstock/importer/intefaces/CMakeLists.txt new file mode 100644 index 0000000..48e82aa --- /dev/null +++ b/src/rollingstock/importer/intefaces/CMakeLists.txt @@ -0,0 +1,13 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + rollingstock/importer/intefaces/icheckname.h + rollingstock/importer/intefaces/iduplicatesitemevent.h + rollingstock/importer/intefaces/iduplicatesitemmodel.h + rollingstock/importer/intefaces/irsimportmodel.h + + rollingstock/importer/intefaces/icheckname.cpp + rollingstock/importer/intefaces/iduplicatesitemevent.cpp + rollingstock/importer/intefaces/iduplicatesitemmodel.cpp + rollingstock/importer/intefaces/irsimportmodel.cpp + PARENT_SCOPE +) diff --git a/src/rollingstock/importer/intefaces/icheckname.cpp b/src/rollingstock/importer/intefaces/icheckname.cpp new file mode 100644 index 0000000..8adcf81 --- /dev/null +++ b/src/rollingstock/importer/intefaces/icheckname.cpp @@ -0,0 +1,16 @@ +#include "icheckname.h" + +ICheckName::~ICheckName() +{ + +} + +bool ICheckName::checkCustomNameValid(db_id, const QString &, const QString &, QString *) +{ + return false; +} + +bool ICheckName::checkNewNumberIsValid(db_id, db_id, db_id, RsType , int, int, QString *) +{ + return false; +} diff --git a/src/rollingstock/importer/intefaces/icheckname.h b/src/rollingstock/importer/intefaces/icheckname.h new file mode 100644 index 0000000..d6ef111 --- /dev/null +++ b/src/rollingstock/importer/intefaces/icheckname.h @@ -0,0 +1,19 @@ +#ifndef ICHECKNAME_H +#define ICHECKNAME_H + +#include +#include "utils/types.h" + +class ICheckName +{ +public: + virtual ~ICheckName(); + + //For models and Owners + virtual bool checkCustomNameValid(db_id importedId, const QString& originalName, const QString& newCustomName, QString *errMsgOut); + + //For RS + virtual bool checkNewNumberIsValid(db_id importedRsId, db_id importedModelId, db_id matchExistingModelId, RsType rsType, int number, int newNumber, QString *errMsgOut); +}; + +#endif // ICHECKNAME_H diff --git a/src/rollingstock/importer/intefaces/iduplicatesitemevent.cpp b/src/rollingstock/importer/intefaces/iduplicatesitemevent.cpp new file mode 100644 index 0000000..e225f18 --- /dev/null +++ b/src/rollingstock/importer/intefaces/iduplicatesitemevent.cpp @@ -0,0 +1,12 @@ +#include "iduplicatesitemevent.h" + +IDuplicatesItemEvent::IDuplicatesItemEvent(QRunnable *self, int pr, int m, int count, int st) : + QEvent(_Type), + task(self), + progress(pr), + max(m), + ducplicatesCount(count), + state(st) +{ + +} diff --git a/src/rollingstock/importer/intefaces/iduplicatesitemevent.h b/src/rollingstock/importer/intefaces/iduplicatesitemevent.h new file mode 100644 index 0000000..9d6a09e --- /dev/null +++ b/src/rollingstock/importer/intefaces/iduplicatesitemevent.h @@ -0,0 +1,33 @@ +#ifndef IDUPLICATESITEMEVENT_H +#define IDUPLICATESITEMEVENT_H + +#include +#include "utils/worker_event_types.h" + +class QRunnable; + +class IDuplicatesItemEvent : public QEvent +{ +public: + static constexpr int MinimumMSecsBeforeFirstEvent = 4000; + + enum + { + ProgressError = -1, + ProgressAbortedByUser = -2, + ProgressMaxFinished = -3 + }; + + static constexpr Type _Type = Type(CustomEvents::RsImportCheckDuplicates); + + IDuplicatesItemEvent(QRunnable *self, int pr, int m, int count, int st); + +public: + QRunnable *task; + int progress; + int max; + int ducplicatesCount; + int state; +}; + +#endif // IDUPLICATESITEMEVENT_H diff --git a/src/rollingstock/importer/intefaces/iduplicatesitemmodel.cpp b/src/rollingstock/importer/intefaces/iduplicatesitemmodel.cpp new file mode 100644 index 0000000..a97dd53 --- /dev/null +++ b/src/rollingstock/importer/intefaces/iduplicatesitemmodel.cpp @@ -0,0 +1,136 @@ +#include "iduplicatesitemmodel.h" + +#include "../model/duplicatesimporteditemsmodel.h" +#include "../model/duplicatesimportedrsmodel.h" + +#include "utils/thread/iquittabletask.h" +#include "iduplicatesitemevent.h" + +#include + +IDuplicatesItemModel::IDuplicatesItemModel(sqlite3pp::database &db, QObject *parent) : + QAbstractTableModel(parent), + mDb(db), + mTask(nullptr), + mState(Loaded), + cachedCount(-1) +{ + +} + +IDuplicatesItemModel::~IDuplicatesItemModel() +{ + if(mTask) + { + mTask->stop(); + mTask->cleanup(); + mTask = nullptr; + } +} + +bool IDuplicatesItemModel::event(QEvent *e) +{ + if(e->type() == IDuplicatesItemEvent::_Type) + { + IDuplicatesItemEvent *ev = static_cast(e); + if(mTask == ev->task) + { + QString errText; + if(ev->max == IDuplicatesItemEvent::ProgressMaxFinished) + { + if(ev->progress == IDuplicatesItemEvent::ProgressError) + { + //FIXME: add error QString in base IQuittableTask + + //errText = loadTask->getErrorText(); + cachedCount = -1; + } + else if(ev->progress != IDuplicatesItemEvent::ProgressAbortedByUser) + { + handleResult(mTask); + cachedCount = ev->ducplicatesCount; + } + + mState = Loaded; + emit stateChanged(mState); + + if(ev->progress == IDuplicatesItemEvent::ProgressAbortedByUser) + { + emit processAborted(); + } + else + { + emit progressFinished(); + } + + if(ev->progress == IDuplicatesItemEvent::ProgressError) + { + //Emit error after finishing + emit error(errText); + } + + //Delete task before handling event because otherwise it is detected as still running + delete mTask; + mTask = nullptr; + } + else + { + if(mState != ev->state) + { + mState = State(ev->state); + cachedCount = ev->ducplicatesCount; + emit stateChanged(mState); + } + emit progressChanged(ev->progress, ev->max); + } + } + } + return QAbstractItemModel::event(e); +} + +IDuplicatesItemModel *IDuplicatesItemModel::createModel(ModelModes::Mode mode, sqlite3pp::database &db, ICheckName *iface, QObject *parent) +{ + switch (mode) + { + case ModelModes::Owners: + case ModelModes::Models: + return new DuplicatesImportedItemsModel(mode, db, iface, parent); + case ModelModes::Rollingstock: + return new DuplicatesImportedRSModel(db, iface, parent); + } + + return nullptr; +} + +bool IDuplicatesItemModel::startLoading(int mode) +{ + if(mState != Loaded) + return true; //Already started + + mTask = createTask(mode); + if(!mTask) + return false; + + cachedCount = -1; + mState = Starting; + emit stateChanged(mState); + + QThreadPool::globalInstance()->start(mTask); + return true; +} + +void IDuplicatesItemModel::cancelLoading() +{ + if(mState == Loaded) + return; //No active process + + //TODO: wait finished? + mTask->stop(); + mTask->cleanup(); + mTask = nullptr; + + emit processAborted(); + + mState = Loaded; + emit stateChanged(mState); +} diff --git a/src/rollingstock/importer/intefaces/iduplicatesitemmodel.h b/src/rollingstock/importer/intefaces/iduplicatesitemmodel.h new file mode 100644 index 0000000..4ea29ed --- /dev/null +++ b/src/rollingstock/importer/intefaces/iduplicatesitemmodel.h @@ -0,0 +1,66 @@ +#ifndef IDUPLICATESITEMMODEL_H +#define IDUPLICATESITEMMODEL_H + +#include + +#include "utils/model_mode.h" + +namespace sqlite3pp { +class database; +} + +class ICheckName; +class IQuittableTask; + +class IDuplicatesItemModel : public QAbstractTableModel +{ + Q_OBJECT +public: + enum State + { + Loaded = 0, + Starting, + CountingItems, + LoadingData, + FixingSameValues + }; + + IDuplicatesItemModel(sqlite3pp::database &db, QObject *parent = nullptr); + ~IDuplicatesItemModel(); + + bool event(QEvent *e) override; + + static IDuplicatesItemModel *createModel(ModelModes::Mode mode, sqlite3pp::database &db, ICheckName *iface, QObject *parent = nullptr); + + inline State getState() const { return mState; } + inline int getItemCount() const + { + if(mState == Loaded || mState == LoadingData) + return cachedCount; + return -1; + } + + bool startLoading(int mode); + void cancelLoading(); + +signals: + void error(const QString& text); + void stateChanged(int state); + void progressChanged(int progress, int max); + void progressFinished(); + void processAborted(); + +protected: + virtual IQuittableTask* createTask(int mode) = 0; + virtual void handleResult(IQuittableTask *task) = 0; + +protected: + sqlite3pp::database &mDb; + +private: + IQuittableTask *mTask; + State mState; + int cachedCount; +}; + +#endif // IDUPLICATESITEMMODEL_H diff --git a/src/rollingstock/importer/intefaces/irsimportmodel.cpp b/src/rollingstock/importer/intefaces/irsimportmodel.cpp new file mode 100644 index 0000000..35dbd27 --- /dev/null +++ b/src/rollingstock/importer/intefaces/irsimportmodel.cpp @@ -0,0 +1,7 @@ +#include "irsimportmodel.h" + +IRsImportModel::IRsImportModel(sqlite3pp::database &db, QObject *parent) : + IPagedItemModel(500, db, parent) +{ + +} diff --git a/src/rollingstock/importer/intefaces/irsimportmodel.h b/src/rollingstock/importer/intefaces/irsimportmodel.h new file mode 100644 index 0000000..c877f9e --- /dev/null +++ b/src/rollingstock/importer/intefaces/irsimportmodel.h @@ -0,0 +1,19 @@ +#ifndef IRSIMPORTMODEL_H +#define IRSIMPORTMODEL_H + +#include "utils/sqldelegate/pageditemmodel.h" +#include "icheckname.h" + +class IRsImportModel : public IPagedItemModel, public ICheckName +{ + Q_OBJECT +public: + IRsImportModel(sqlite3pp::database &db, QObject *parent = nullptr); + + virtual int countImported() = 0; + +signals: + void importCountChanged(); +}; + +#endif // IRSIMPORTMODEL_H diff --git a/src/rollingstock/importer/model/CMakeLists.txt b/src/rollingstock/importer/model/CMakeLists.txt new file mode 100644 index 0000000..aa33d8c --- /dev/null +++ b/src/rollingstock/importer/model/CMakeLists.txt @@ -0,0 +1,15 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + rollingstock/importer/model/duplicatesimporteditemsmodel.h + rollingstock/importer/model/duplicatesimportedrsmodel.h + rollingstock/importer/model/rsimportedmodelsmodel.h + rollingstock/importer/model/rsimportedownersmodel.h + rollingstock/importer/model/rsimportedrollingstockmodel.h + + rollingstock/importer/model/duplicatesimporteditemsmodel.cpp + rollingstock/importer/model/duplicatesimportedrsmodel.cpp + rollingstock/importer/model/rsimportedmodelsmodel.cpp + rollingstock/importer/model/rsimportedownersmodel.cpp + rollingstock/importer/model/rsimportedrollingstockmodel.cpp + PARENT_SCOPE +) diff --git a/src/rollingstock/importer/model/duplicatesimporteditemsmodel.cpp b/src/rollingstock/importer/model/duplicatesimporteditemsmodel.cpp new file mode 100644 index 0000000..9842c39 --- /dev/null +++ b/src/rollingstock/importer/model/duplicatesimporteditemsmodel.cpp @@ -0,0 +1,372 @@ +#include "duplicatesimporteditemsmodel.h" + +#include "../rsimportstrings.h" + +#include "rollingstock/importer/intefaces/icheckname.h" + +#include "utils/thread/iquittabletask.h" +#include "../intefaces/iduplicatesitemevent.h" + +#include + +#include + +class DuplicatesImportedItemsModelTask : public IQuittableTask +{ +public: + QVector items; + ModelModes::Mode m_mode; + + inline DuplicatesImportedItemsModelTask(sqlite3pp::database &db, ModelModes::Mode mode, QObject *receiver) : + IQuittableTask(receiver), + m_mode(mode), + mDb(db) + { + + } + + void run() override + { + QElapsedTimer timer; + timer.start(); + + int count = -1; + IDuplicatesItemModel::State state = IDuplicatesItemModel::CountingItems; + + if(wasStopped()) + { + sendEvent(new IDuplicatesItemEvent(this, + IDuplicatesItemEvent::ProgressAbortedByUser, + IDuplicatesItemEvent::ProgressMaxFinished, + count, state), + true); + return; + } + + //Inform model that task is started + int max = 100; + int progress = 0; + sendEvent(new IDuplicatesItemEvent(this, progress, max, count, state), false); + + //Count how many items + QByteArray sql = "SELECT COUNT(imp.id)," + " (CASE WHEN imp.new_name NOT NULL THEN imp.new_name ELSE imp.name END) AS name1," + " (CASE WHEN dup.new_name NOT NULL THEN dup.new_name ELSE dup.name END) AS name2" + " FROM %1 imp" + " JOIN %1 dup ON dup.id<>imp.id AND %2 name1=name2" + " WHERE imp.match_existing_id IS NULL AND imp.import=1 AND dup.import=1"; + if(m_mode == ModelModes::Models) + { + sql = sql.replace("%1", "imported_rs_models"); + sql = sql.replace("%2", "imp.suffix=dup.suffix AND"); + }else{ + sql = sql.replace("%1", "imported_rs_owners"); + sql = sql.replace("%2", " "); + } + + query q(mDb, sql); + q.step(); + count = q.getRows().get(0); + + if(wasStopped()) + { + sendEvent(new IDuplicatesItemEvent(this, + IDuplicatesItemEvent::ProgressAbortedByUser, + IDuplicatesItemEvent::ProgressMaxFinished, + count, state), + true); + return; + } + + if(count == 0) + { + //No data to load, finish + state = IDuplicatesItemModel::Loaded; + sendEvent(new IDuplicatesItemEvent(this, progress, IDuplicatesItemEvent::ProgressMaxFinished, + count, state), + true); + return; + } + + //Now load data + state = IDuplicatesItemModel::LoadingData; + max = count + 10; + progress = 10; + + //Do not send event if the process is fast + if(timer.elapsed() > IDuplicatesItemEvent::MinimumMSecsBeforeFirstEvent) + sendEvent(new IDuplicatesItemEvent(this, progress, max, count, state), false); + + items.reserve(count); + + sql = "SELECT imp.id, imp.name, imp.new_name, %2" + " (CASE WHEN imp.new_name NOT NULL THEN imp.new_name ELSE imp.name END) AS name1," + " (CASE WHEN dup.new_name NOT NULL THEN dup.new_name ELSE dup.name END) AS name2" + " FROM %1 imp" + " JOIN %1 dup ON dup.id<>imp.id AND %3 name1=name2" + " WHERE imp.match_existing_id IS NULL AND imp.import=1 AND dup.import=1" + " ORDER BY imp.name"; + if(m_mode == ModelModes::Models) + { + sql = sql.replace("%1", "imported_rs_models"); + sql = sql.replace("%2", " "); + sql = sql.replace("%3", "imp.suffix=dup.suffix AND"); + }else{ + sql = sql.replace("%1", "imported_rs_owners"); + sql = sql.replace("%2", "imp.sheet_idx,"); + sql = sql.replace("%3", " "); + } + + q.prepare(sql); + + //Send about 5 progress events during loading (but process at least 5 items between 2 events) + const int sentTreshold = qMax(5, max / 5); + + for(auto r : q) + { + if(progress % 8 == 0 && wasStopped()) + { + sendEvent(new IDuplicatesItemEvent(this, + IDuplicatesItemEvent::ProgressAbortedByUser, + IDuplicatesItemEvent::ProgressMaxFinished, + count, state), + true); + return; + } + + if(progress % sentTreshold && timer.elapsed() > IDuplicatesItemEvent::MinimumMSecsBeforeFirstEvent) + { + //It's time to report our progress + sendEvent(new IDuplicatesItemEvent(this, progress, max, count, state), false); + } + + DuplicatesImportedItemsModel::DuplicatedItem item; + item.importedId = r.get(0); + item.originalName = r.get(1); + item.customName = r.get(2); + item.sheetIdx = m_mode == ModelModes::Owners ? r.get(3) : 0; + item.import = true; + + items.append(item); + } + + state = IDuplicatesItemModel::Loaded; + sendEvent(new IDuplicatesItemEvent(this, progress, IDuplicatesItemEvent::ProgressMaxFinished, + count, state), + true); + } + +private: + sqlite3pp::database &mDb; +}; + +DuplicatesImportedItemsModel::DuplicatesImportedItemsModel(ModelModes::Mode mode, database &db, ICheckName *i, QObject *parent): + IDuplicatesItemModel(db, parent), + iface(i), + m_mode(mode) +{ +} + +QVariant DuplicatesImportedItemsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + if(m_mode == ModelModes::Models && section >= SheetIdx) + section++; //Skip SheetIdx for Models + + switch (section) + { + case Import: + return RsImportStrings::tr("Import"); + case SheetIdx: + return RsImportStrings::tr("Sheet No."); + case Name: + return RsImportStrings::tr("Name"); + case CustomName: + return RsImportStrings::tr("Custom Name"); + } + } + return QAbstractTableModel::headerData(section, orientation, role); +} + +int DuplicatesImportedItemsModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : items.size(); +} + +int DuplicatesImportedItemsModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_mode == ModelModes::Models ? NCols - 1 : NCols; +} + +QVariant DuplicatesImportedItemsModel::data(const QModelIndex &idx, int role) const +{ + int col = idx.column(); + if(m_mode == ModelModes::Models && col >= SheetIdx) + col++; //Skip SheetIdx for Models + + if (!idx.isValid() || idx.row() >= items.size() || col >= NCols) + return QVariant(); + + const DuplicatedItem& item = items.at(idx.row()); + + switch (role) + { + case Qt::DisplayRole: + { + switch (col) + { + case SheetIdx: + return item.sheetIdx; + case Name: + return item.originalName; + case CustomName: + return item.customName; + } + break; + } + case Qt::EditRole: + { + if(col == CustomName) + return item.customName; + break; + } + case Qt::BackgroundRole: + { + if(!item.import || (idx.column() == CustomName && item.customName.isEmpty())) + return QBrush(Qt::lightGray); + break; + } + case Qt::CheckStateRole: + { + if(col == Import) + return item.import ? Qt::Checked : Qt::Unchecked; + break; + } + } + return QVariant(); +} + +bool DuplicatesImportedItemsModel::setData(const QModelIndex &idx, const QVariant &value, int role) +{ + int col = idx.column(); + if(m_mode == ModelModes::Models && col >= SheetIdx) + col++; //Skip SheetIdx for Models + + if (!idx.isValid() || idx.row() >= items.size()) + return false; + + QModelIndex first = idx; + QModelIndex last = idx; + + DuplicatedItem& item =items[idx.row()]; + + switch (role) + { + case Qt::EditRole: + { + if(col == CustomName) + { + QString newName = value.toString().simplified(); + if(item.customName == newName) + return false; + + QString errText; + if(!iface->checkCustomNameValid(item.importedId, item.originalName, newName, &errText)) + { + emit error(errText); + return false; + } + + const char *sql[NModes] = { + "UPDATE imported_rs_owners SET new_name=? WHERE id=?", + + "UPDATE imported_rs_models SET new_name=? WHERE id=?" + }; + + command set_name(mDb, sql[m_mode]); + set_name.bind(1, newName); + set_name.bind(2, item.importedId); + if(set_name.execute() != SQLITE_OK) + return false; + + item.customName = newName; + } + break; + } + case Qt::CheckStateRole: + { + if(col == Import) + { + Qt::CheckState cs = value.value(); + const bool import = cs == Qt::Checked; + if(item.import == import) + return false; //No change + + if(import) + { + //Newly imported, check if there are duplicates + QString errText; + if(!iface->checkCustomNameValid(item.importedId, item.originalName, item.customName, &errText)) + { + emit error(errText); + return false; + } + } + + const char *sql[NModes] = { + "UPDATE imported_rs_owners SET import=? WHERE id=?", + + "UPDATE imported_rs_models SET import=? WHERE id=?" + }; + + command set_imported(mDb, sql[m_mode]); + set_imported.bind(1, import ? 1 : 0); + set_imported.bind(2, item.importedId); + if(set_imported.execute() != SQLITE_OK) + return false; + + item.import = import; + + //Update all columns to update background + first = index(idx.row(), 0); + //Do not use NCols because in Models mode SheetIdx is hidden + last = index(idx.row(), columnCount()); + } + break; + } + } + + emit dataChanged(first, last); + return true; +} + +Qt::ItemFlags DuplicatesImportedItemsModel::flags(const QModelIndex &idx) const +{ + if (!idx.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags f = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemNeverHasChildren; + int col = idx.column(); + if(m_mode == ModelModes::Models && col >= SheetIdx) + col++; //Skip SheetIdx for Models + if(col == Import) + f.setFlag(Qt::ItemIsUserCheckable); + if(col == CustomName) + f.setFlag(Qt::ItemIsEditable); + + return f; +} + +IQuittableTask *DuplicatesImportedItemsModel::createTask(int mode) +{ + Q_UNUSED(mode) //Only 1 mode possible + return new DuplicatesImportedItemsModelTask(mDb, m_mode, this); +} + +void DuplicatesImportedItemsModel::handleResult(IQuittableTask *task) +{ + beginResetModel(); + items = static_cast(task)->items; + endResetModel(); +} diff --git a/src/rollingstock/importer/model/duplicatesimporteditemsmodel.h b/src/rollingstock/importer/model/duplicatesimporteditemsmodel.h new file mode 100644 index 0000000..3b86d10 --- /dev/null +++ b/src/rollingstock/importer/model/duplicatesimporteditemsmodel.h @@ -0,0 +1,76 @@ +#ifndef DUPLICATESIMPORTEDITEMSMODEL_H +#define DUPLICATESIMPORTEDITEMSMODEL_H + +#include +#include + +#include "utils/types.h" +#include "utils/model_mode.h" + +#include "../intefaces/iduplicatesitemmodel.h" + +#include +using namespace sqlite3pp; + +class ICheckName; + +class DuplicatesImportedItemsModel : public IDuplicatesItemModel +{ + Q_OBJECT + +public: + enum{ + NModes = 2 + }; + + enum Columns + { + Import = 0, + SheetIdx, + Name, + CustomName, + NCols + }; + + typedef struct + { + db_id importedId; + QString originalName; + QString customName; + int sheetIdx; + bool import; + } DuplicatedItem; + + DuplicatesImportedItemsModel(ModelModes::Mode mode, database &db, ICheckName *i, QObject *parent = nullptr); + + // 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; + + // Editable: + bool setData(const QModelIndex &idx, const QVariant &value, + int role = Qt::EditRole) override; + + Qt::ItemFlags flags(const QModelIndex& idx) const override; + +protected: + // IDuplicatesItemModel + IQuittableTask* createTask(int mode) override; + void handleResult(IQuittableTask *task) override; + +private: + ICheckName *iface; + + QVector items; + + ModelModes::Mode m_mode; +}; + +#endif // DUPLICATESIMPORTEDITEMSMODEL_H diff --git a/src/rollingstock/importer/model/duplicatesimportedrsmodel.cpp b/src/rollingstock/importer/model/duplicatesimportedrsmodel.cpp new file mode 100644 index 0000000..b96e7be --- /dev/null +++ b/src/rollingstock/importer/model/duplicatesimportedrsmodel.cpp @@ -0,0 +1,460 @@ +#include "duplicatesimportedrsmodel.h" + +#include "rollingstock/importer/intefaces/icheckname.h" + +#include "utils/thread/iquittabletask.h" +#include "../intefaces/iduplicatesitemevent.h" + +#include "../rsimportstrings.h" +#include "utils/rs_utils.h" + +#include + +#include + + +class DuplicatesImportedRSModelTask : public IQuittableTask +{ +public: + QVector items; + + inline DuplicatesImportedRSModelTask(int mode, sqlite3pp::database &db, QObject *receiver) : + IQuittableTask(receiver), + mDb(db) + { + fixItemsWithSameValues = (mode == IDuplicatesItemModel::FixingSameValues); + } + + void run() override + { + timer.start(); + + int count = -1; + IDuplicatesItemModel::State state = IDuplicatesItemModel::CountingItems; + + if(wasStopped()) + { + sendEvent(new IDuplicatesItemEvent(this, + IDuplicatesItemEvent::ProgressAbortedByUser, + IDuplicatesItemEvent::ProgressMaxFinished, + count, state), + true); + return; + } + + //Inform model that task is started + int max = 100; + int progress = 0; + sendEvent(new IDuplicatesItemEvent(this, progress++, max, count, state), false); + + if(fixItemsWithSameValues) + execFixItemsWithSameValues(); + + //Count how many items + query q(mDb, "SELECT COUNT(1) FROM (" + "SELECT (CASE WHEN imp.new_number NOT NULL THEN imp.new_number ELSE imp.number END) AS num," + " (CASE WHEN dup.new_number NOT NULL THEN dup.new_number ELSE dup.number END) AS dup_num" + " FROM imported_rs_list imp" + " JOIN imported_rs_models mod1 ON mod1.id=imp.model_id" + " JOIN imported_rs_owners own1 ON own1.id=imp.owner_id" + + " LEFT JOIN imported_rs_list dup ON dup.id<>imp.id" + " LEFT JOIN imported_rs_models mod2 ON mod2.id=dup.model_id" + " LEFT JOIN imported_rs_owners own2 ON own2.id=dup.owner_id" + " LEFT JOIN rs_list ON rs_list.model_id=mod1.match_existing_id" + " WHERE own1.import AND mod1.import AND imp.import=1 AND" + " (" + " rs_list.number=num OR" + " (" + " (" + " imp.model_id=dup.model_id OR" + " (" + " mod1.match_existing_id NOT NULL AND mod1.match_existing_id=mod2.match_existing_id" + " )" + " )" + " AND num=dup_num AND own2.import=1 AND mod2.import=1 AND dup.import=1" + " )" + " )" + " GROUP BY imp.id)"); + q.step(); + count = q.getRows().get(0); + if(wasStopped()) + { + sendEvent(new IDuplicatesItemEvent(this, + IDuplicatesItemEvent::ProgressAbortedByUser, + IDuplicatesItemEvent::ProgressMaxFinished, + count, state), + true); + return; + } + + if(count == 0) + { + //No data to load, finish + state = IDuplicatesItemModel::Loaded; + sendEvent(new IDuplicatesItemEvent(this, progress, IDuplicatesItemEvent::ProgressMaxFinished, + count, state), + true); + return; + } + + //Now load data + state = IDuplicatesItemModel::LoadingData; + max = count + 10; + progress = 10; + + //Do not send event if the process is fast + if(timer.elapsed() > IDuplicatesItemEvent::MinimumMSecsBeforeFirstEvent) + sendEvent(new IDuplicatesItemEvent(this, progress, max, count, state), false); + + items.reserve(count); + + q.prepare("SELECT imp.id, mod1.id, mod1.match_existing_id, mod1.type," + " mod1.name, mod1.new_name, rs_models.name, rs_models.type," + " imp.number, imp.new_number," + " own1.name, own1.new_name, rs_owners.name," + " (CASE WHEN imp.new_number NOT NULL THEN imp.new_number ELSE imp.number END) AS num," + " (CASE WHEN dup.new_number NOT NULL THEN dup.new_number ELSE dup.number END) AS dup_num" + " FROM imported_rs_list imp" + " JOIN imported_rs_models mod1 ON mod1.id=imp.model_id" + " JOIN imported_rs_owners own1 ON own1.id=imp.owner_id" + + " LEFT JOIN imported_rs_list dup ON dup.id<>imp.id" + " LEFT JOIN imported_rs_models mod2 ON mod2.id=dup.model_id" + " LEFT JOIN imported_rs_owners own2 ON own2.id=dup.owner_id" + " LEFT JOIN rs_list ON rs_list.model_id=mod1.match_existing_id" + + " LEFT JOIN rs_owners ON rs_owners.id=own1.match_existing_id" + " LEFT JOIN rs_models ON rs_models.id=mod1.match_existing_id" + + " WHERE own1.import AND mod1.import AND imp.import=1 AND" + " (" + " rs_list.number=num OR" + " (" + " (" + " imp.model_id=dup.model_id OR" + " (" + " mod1.match_existing_id NOT NULL AND mod1.match_existing_id=mod2.match_existing_id" + " )" + " )" + " AND num=dup_num AND own2.import=1 AND mod2.import=1 AND dup.import=1" + " )" + " )" + " GROUP BY imp.id" + " ORDER BY mod1.name, imp.number, own1.name"); + sqlite3_stmt *st = q.stmt(); + + //Send about 5 progress events during loading (but process at least 5 items between 2 events) + const int sentTreshold = qMax(5, max / 5); + + for(auto r : q) + { + if(progress % 8 == 0 && wasStopped()) + { + sendEvent(new IDuplicatesItemEvent(this, + IDuplicatesItemEvent::ProgressAbortedByUser, + IDuplicatesItemEvent::ProgressMaxFinished, + count, state), + true); + return; + } + + if(progress % sentTreshold && timer.elapsed() > IDuplicatesItemEvent::MinimumMSecsBeforeFirstEvent) + { + //It's time to report our progress + sendEvent(new IDuplicatesItemEvent(this, progress, max, count, state), false); + } + + DuplicatesImportedRSModel::DuplicatedItem item; + item.importedId = r.get(0); + item.importedModelId = r.get(1); + + //Model + item.matchExistingModelId = r.get(2); + item.type = RsType(r.get(3)); + item.modelName = QByteArray(reinterpret_cast(sqlite3_column_text(st, 4)), + sqlite3_column_bytes(st, 4)); + if(r.column_type(5) != SQLITE_NULL) + { + // 'name (new_name)' + item.modelName.append(" (", 2); + item.modelName.append(reinterpret_cast(sqlite3_column_text(st, 5)), + sqlite3_column_bytes(st, 5)); + item.modelName.append(')'); + } + + if(r.column_type(6) != SQLITE_NULL) + { + // 'name (match_existing name)' + QByteArray tmp = QByteArray::fromRawData(reinterpret_cast(sqlite3_column_text(st, 6)), + sqlite3_column_bytes(st, 6)); + + if(tmp != item.modelName) + { + item.modelName.append(" (", 2); + item.modelName.append(tmp); + item.modelName.append(')'); + } + //Prefer matched model type when available + item.type = RsType(r.get(7)); + } + + //Number + item.number = r.get(8); + + if(r.column_type(9) == SQLITE_NULL) + item.new_number = -1; + else + item.new_number = r.get(9); + + item.ownerName = QByteArray(reinterpret_cast(sqlite3_column_text(st, 10)), + sqlite3_column_bytes(st, 10)); + if(r.column_type(11) != SQLITE_NULL) + { + // 'name (new_name)' + item.ownerName.append(" (", 2); + item.ownerName.append(reinterpret_cast(sqlite3_column_text(st, 11)), + sqlite3_column_bytes(st, 11)); + item.ownerName.append(')'); + } + + if(r.column_type(12) != SQLITE_NULL) + { + // 'name (match_existing name)' + QByteArray tmp = QByteArray::fromRawData(reinterpret_cast(sqlite3_column_text(st, 12)), + sqlite3_column_bytes(st, 12)); + + if(tmp != item.ownerName) + { + item.ownerName.append(" (", 2); + item.ownerName.append(tmp); + item.ownerName.append(')'); + } + } + item.import = true; //Only imported items get selected so import is true + + items.append(item); + + progress++; + } + + state = IDuplicatesItemModel::Loaded; + sendEvent(new IDuplicatesItemEvent(this, progress, IDuplicatesItemEvent::ProgressMaxFinished, + count, state), + true); + } + + void execFixItemsWithSameValues() + { + //If 2 (or mode) rollingstock items have + //same model and same number and same owner + //pick one of them and discart the other(s) + //TODO: implement and add button to FixDuplicatesDlg to trigger this mode + //And re-evalue progress values + } + +private: + sqlite3pp::database &mDb; + QElapsedTimer timer; + bool fixItemsWithSameValues; +}; + +DuplicatesImportedRSModel::DuplicatesImportedRSModel(database &db, ICheckName *i, QObject *parent): + IDuplicatesItemModel(db, parent), + iface(i) +{ + +} + +QVariant DuplicatesImportedRSModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch (section) + { + case Import: + return RsImportStrings::tr("Import"); + case ModelName: + return RsImportStrings::tr("Model"); + case Number: + return RsImportStrings::tr("Number"); + case NewNumber: + return RsImportStrings::tr("New number"); + case OwnerName: + return RsImportStrings::tr("Owner"); + } + } + return QAbstractTableModel::headerData(section, orientation, role); +} + +int DuplicatesImportedRSModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : items.size(); +} + +int DuplicatesImportedRSModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant DuplicatesImportedRSModel::data(const QModelIndex &idx, int role) const +{ + if (!idx.isValid() || idx.row() >= items.size() || idx.column() >= NCols) + return QVariant(); + + const DuplicatedItem& item = items.at(idx.row()); + + switch (role) + { + case Qt::DisplayRole: + { + switch (idx.column()) + { + case ModelName: + return item.modelName; + case Number: + return rs_utils::formatNum(item.type, item.number); + case NewNumber: + return item.new_number == -1 ? QVariant() : rs_utils::formatNum(item.type, item.new_number); + case OwnerName: + return item.ownerName; + } + break; + } + case Qt::EditRole: + { + if(idx.column() == NewNumber) + return item.new_number; + break; + } + case Qt::TextAlignmentRole: + { + if(idx.column() == Number || idx.column() == NewNumber) + return Qt::AlignRight + Qt::AlignVCenter; + break; + } + case Qt::BackgroundRole: + { + if(!item.import || (idx.column() == NewNumber && item.new_number == -1)) + return QBrush(Qt::lightGray); + break; + } + case Qt::CheckStateRole: + { + switch (idx.column()) + { + case Import: + return item.import ? Qt::Checked : Qt::Unchecked; + } + break; + } + } + return QVariant(); +} + +bool DuplicatesImportedRSModel::setData(const QModelIndex &idx, const QVariant &value, int role) +{ + if (!idx.isValid() || idx.row() >= items.size()) + return false; + + DuplicatedItem& item =items[idx.row()]; + + switch (role) + { + case Qt::EditRole: + { + if(idx.column() == NewNumber) + { + int newNumber = value.toInt(); + + if(item.new_number == newNumber) + return false; + + QString errText; + if(!iface->checkNewNumberIsValid(item.importedId, item.importedModelId, item.matchExistingModelId, + item.type, item.number, newNumber, &errText)) + { + emit error(errText); + return false; + } + + command set_newNumber(mDb, "UPDATE imported_rs_list SET new_number=? WHERE id=?"); + if(newNumber == -1) + set_newNumber.bind(1); //Bind NULL + else + set_newNumber.bind(1, newNumber); + set_newNumber.bind(2, item.importedId); + if(set_newNumber.execute() != SQLITE_OK) + return false; + + item.new_number = newNumber; + emit dataChanged(idx, idx); + } + break; + } + case Qt::CheckStateRole: + { + if(idx.column() == Import) + { + Qt::CheckState cs = value.value(); + const bool import = cs == Qt::Checked; + if(item.import == import) + return false; //No change + + if(import) + { + //Newly imported, check if there are duplicates + QString errText; + if(!iface->checkNewNumberIsValid(item.importedId, item.importedModelId, item.matchExistingModelId, + item.type, item.number, item.new_number, &errText)) + { + emit error(errText); + return false; + } + } + + command set_imported(mDb, "UPDATE imported_rs_list SET import=? WHERE id=?"); + set_imported.bind(1, import ? 1 : 0); + set_imported.bind(2, item.importedId); + if(set_imported.execute() != SQLITE_OK) + return false; + + item.import = import; + + //Update all columns to update background + QModelIndex first = index(idx.row(), 0); + QModelIndex last = index(idx.row(), NCols - 1); + emit dataChanged(first, last); + } + break; + } + } + + return true; +} + +Qt::ItemFlags DuplicatesImportedRSModel::flags(const QModelIndex &idx) const +{ + if (!idx.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags f = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemNeverHasChildren; + if(idx.column() == NewNumber) + f.setFlag(Qt::ItemIsEditable); + else if(idx.column() == Import) + f.setFlag(Qt::ItemIsUserCheckable); + + return f; +} + +IQuittableTask *DuplicatesImportedRSModel::createTask(int mode) +{ + return new DuplicatesImportedRSModelTask(mode, mDb, this); +} + +void DuplicatesImportedRSModel::handleResult(IQuittableTask *task) +{ + beginResetModel(); + items = static_cast(task)->items; + endResetModel(); +} diff --git a/src/rollingstock/importer/model/duplicatesimportedrsmodel.h b/src/rollingstock/importer/model/duplicatesimportedrsmodel.h new file mode 100644 index 0000000..229c0f9 --- /dev/null +++ b/src/rollingstock/importer/model/duplicatesimportedrsmodel.h @@ -0,0 +1,72 @@ +#ifndef DUPLICATESIMPORTEDRSMODEL_H +#define DUPLICATESIMPORTEDRSMODEL_H + +#include +#include + +#include "utils/types.h" + +#include "../intefaces/iduplicatesitemmodel.h" + +#include +using namespace sqlite3pp; + +class ICheckName; + +class DuplicatesImportedRSModel : public IDuplicatesItemModel +{ + Q_OBJECT + +public: + enum Columns + { + Import = 0, + ModelName, + Number, + NewNumber, + OwnerName, + NCols + }; + + typedef struct + { + db_id importedId; + db_id importedModelId; + db_id matchExistingModelId; + QByteArray modelName; + QByteArray ownerName; + int number; + int new_number; + RsType type; + bool import; + } DuplicatedItem; + + DuplicatesImportedRSModel(database &db, ICheckName *i, QObject *parent = nullptr); + + // 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; + + // Editable: + bool setData(const QModelIndex &idx, const QVariant &value, + int role = Qt::EditRole) override; + + Qt::ItemFlags flags(const QModelIndex& idx) const override; + +protected: + // IDuplicatesItemModel + IQuittableTask* createTask(int mode) override; + void handleResult(IQuittableTask *task) override; + +private: + ICheckName *iface; + + QVector items; +}; + +#endif // DUPLICATESIMPORTEDRSMODEL_H diff --git a/src/rollingstock/importer/model/rsimportedmodelsmodel.cpp b/src/rollingstock/importer/model/rsimportedmodelsmodel.cpp new file mode 100644 index 0000000..02856ef --- /dev/null +++ b/src/rollingstock/importer/model/rsimportedmodelsmodel.cpp @@ -0,0 +1,780 @@ +#include "rsimportedmodelsmodel.h" + +#include "../rsimportstrings.h" + +#include +#include "utils/worker_event_types.h" + +#include + +#include + +#include "utils/rs_types_names.h" + +#include + +class ModelsResultEvent : public QEvent +{ +public: + static constexpr Type _Type = Type(CustomEvents::RsImportedModelsResult); + inline ModelsResultEvent() : QEvent(_Type) {} + + QVector items; + int firstRow; +}; + +RSImportedModelsModel::RSImportedModelsModel(database &db, QObject *parent) : + IRsImportModel(db, parent), + cacheFirstRow(0), + firstPendingRow(-BatchSize) +{ + sortColumn = Name; +} + +bool RSImportedModelsModel::event(QEvent *e) +{ + if(e->type() == ModelsResultEvent::_Type) + { + ModelsResultEvent *ev = static_cast(e); + ev->setAccepted(true); + + handleResult(ev->items, ev->firstRow); + + return true; + } + + return QAbstractTableModel::event(e); +} + +void RSImportedModelsModel::fetchRow(int row) +{ + if(row >= firstPendingRow && row < firstPendingRow + BatchSize) + return; //Already fetching + + 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; + ModelItem *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 (sortColumn) + { + case Name: + { + if(item) + { + val = item->name; + } + break; + } + //No data hint for other columns + } + + //TODO: use a custom QRunnable + QMetaObject::invokeMethod(this, "internalFetch", Qt::QueuedConnection, + Q_ARG(int, firstPendingRow), Q_ARG(int, sortColumn), + Q_ARG(int, valRow), Q_ARG(QVariant, val)); +} + +void RSImportedModelsModel::internalFetch(int first, int sortCol, int valRow, const QVariant& val) +{ + query q(mDb); + + int offset = first - valRow; + bool reverse = false; + + if(valRow > first) + { + offset = 0; + reverse = true; + } + + //FIXME: maybe show cyan background if there aren't RS of this model (like if owner or RS is not imported), and do the same for owners + + qDebug() << "Fetching:" << first << "ValRow:" << valRow << val << "Offset:" << offset << "Reverse:" << reverse; + + const char *whereCol; + + QByteArray sql = "SELECT imp.id,imp.name,imp.suffix,imp.import,imp.new_name,imp.match_existing_id,rs_models.name," + " imp.max_speed,imp.axes,imp.type,imp.sub_type" + " FROM imported_rs_models imp LEFT JOIN rs_models ON rs_models.id=imp.match_existing_id"; + switch (sortCol) + { + case Name: + { + whereCol = "imp.name"; + break; + } + case Import: + { + whereCol = "imp.import DESC, imp.name"; //Order by 2 columns, no where clause + break; + } + case MaxSpeedCol: + { + whereCol = "imp.max_speed"; + break; + } + case AxesCol: + { + whereCol = "imp.axes"; + break; + } + case TypeCol: + { + whereCol = "imp.type,imp.sub_type"; //Order by 2 columns, no where clause + break; + } + } + + if(val.isValid()) + { + sql += " WHERE "; + sql += whereCol; + if(reverse) + 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); + + 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; + ModelItem &item = vec[i]; + item.importdModelId = r.get(0); + item.name = r.get(1); + item.suffix = r.get(2); + item.import = r.get(3) != 0; + if(r.column_type(4) != SQLITE_NULL) + item.customName = r.get(4); + item.matchExistingId = r.get(5); + if(item.matchExistingId) + item.matchExistingName = r.get(6); + item.maxSpeedKmH = qint16(r.get(7)); + item.axes = qint8(r.get(8)); + item.type = RsType(r.get(9)); + item.subType = RsEngineSubType(r.get(10)); + i--; + } + if(i > -1) + vec.remove(0, i + 1); + } + else + { + int i = 0; + + for(; it != end; ++it) + { + auto r = *it; + ModelItem &item = vec[i]; + item.importdModelId = r.get(0); + item.name = r.get(1); + item.suffix = r.get(2); + item.import = r.get(3) != 0; + if(r.column_type(4) != SQLITE_NULL) + item.customName = r.get(4); + item.matchExistingId = r.get(5); + if(item.matchExistingId) + item.matchExistingName = r.get(6); + item.maxSpeedKmH = qint16(r.get(7)); + item.axes = qint8(r.get(8)); + item.type = RsType(r.get(9)); + item.subType = RsEngineSubType(r.get(10)); + i++; + } + if(i < BatchSize) + vec.remove(i, BatchSize - i); + } + + + ModelsResultEvent *ev = new ModelsResultEvent; + ev->items = vec; + ev->firstRow = first; + + qApp->postEvent(this, ev); +} + +void RSImportedModelsModel::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; + + QModelIndex firstIdx = index(firstRow, 0); + QModelIndex lastIdx = index(firstRow + items.count() - 1, NCols - 1); + emit dataChanged(firstIdx, lastIdx); + + qDebug() << "TOTAL: From:" << cacheFirstRow << "To:" << cacheFirstRow + cache.size() - 1; +} + +QVariant RSImportedModelsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch (section) + { + case Import: + return RsImportStrings::tr("Import"); + case Name: + return RsImportStrings::tr("Name"); + case CustomName: + return RsImportStrings::tr("Custom Name"); + case MatchExisting: + return RsImportStrings::tr("Match Existing"); + case SuffixCol: + return RsTypeNames::tr("Suffix"); + case MaxSpeedCol: + return RsTypeNames::tr("Max Speed"); + case AxesCol: + return RsTypeNames::tr("Axes"); + case TypeCol: + return RsTypeNames::tr("Type"); + case SubTypeCol: + return RsTypeNames::tr("Sub Type"); + } + } + return QAbstractTableModel::headerData(section, orientation, role); +} + +int RSImportedModelsModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : curItemCount; +} + +int RSImportedModelsModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant RSImportedModelsModel::data(const QModelIndex &idx, int role) 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 QVariant("..."); + } + + const ModelItem& item = cache.at(row - cacheFirstRow); + + switch (role) + { + case Qt::DisplayRole: + { + switch (idx.column()) + { + case Name: + return item.name; + case CustomName: + return item.customName; + case MatchExisting: + return item.matchExistingName; + case SuffixCol: + return item.suffix; + case MaxSpeedCol: + return item.maxSpeedKmH; + case AxesCol: + return item.axes; + case TypeCol: + return RsTypeNames::name(item.type); + case SubTypeCol: + if(item.type != RsType::Engine) + break; + return RsTypeNames::name(item.subType); + } + break; + } + case Qt::EditRole: + { + switch (idx.column()) + { + case CustomName: + return item.customName; + } + break; + } + case Qt::BackgroundRole: + { + if(!item.import || + (idx.column() == CustomName && item.customName.isEmpty()) || + (idx.column() == MatchExisting && item.matchExistingId == 0)) + return QBrush(Qt::lightGray); //If not imported mark background or no custom/matching name set + break; + } + case Qt::CheckStateRole: + { + switch (idx.column()) + { + case Import: + return item.import ? Qt::Checked : Qt::Unchecked; + } + break; + } + } + + return QVariant(); +} + +bool RSImportedModelsModel::setData(const QModelIndex &idx, const QVariant &value, int role) +{ + const int row = idx.row(); + if(!idx.isValid() || row >= curItemCount || idx.column() >= NCols || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + ModelItem &item = cache[row - cacheFirstRow]; + QModelIndex first = idx; + QModelIndex last = idx; + + switch (role) + { + case Qt::EditRole: + { + switch (idx.column()) + { + case CustomName: + { + const QString newName = value.toString().simplified(); + if(item.customName == newName) + return false; //No change + + QString errText; + if(!checkCustomNameValid(item.importdModelId, item.name, newName, &errText)) + { + emit modelError(errText); + return false; + } + + command set_name(mDb, "UPDATE imported_rs_models SET new_name=?,match_existing_id=NULL WHERE id=?"); + + if(newName.isEmpty()) + set_name.bind(1); //Bind NULL + else + set_name.bind(1, newName); + set_name.bind(2, item.importdModelId); + if(set_name.execute() != SQLITE_OK) + return false; + + item.customName = newName; + item.matchExistingId = 0; + item.matchExistingName.clear(); + item.matchExistingName.squeeze(); + + last = index(row, MatchExisting); + + break; + } + default: + return false; + } + break; + } + case Qt::CheckStateRole: + { + switch (idx.column()) + { + case Import: + { + Qt::CheckState cs = value.value(); + const bool import = cs == Qt::Checked; + if(item.import == import) + return false; //No change + + if(import) + { + //Newly imported, check for duplicates + QString errText; + if(!checkCustomNameValid(item.importdModelId, item.name, item.customName, &errText)) + { + emit modelError(errText); + return false; + } + } + + command set_imported(mDb, "UPDATE imported_rs_models SET import=? WHERE id=?"); + set_imported.bind(1, import ? 1 : 0); + set_imported.bind(2, item.importdModelId); + if(set_imported.execute() != SQLITE_OK) + return false; + + item.import = import; + + if(sortColumn == Import) + { + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + } + + emit importCountChanged(); + + //Update all columns to update background + first = index(row, 0); + last = index(row, NCols - 1); + break; + } + default: + return false; + } + break; + } + default: + return false; + } + + emit dataChanged(first, last); + return true; +} + +Qt::ItemFlags RSImportedModelsModel::flags(const QModelIndex &idx) const +{ + if (!idx.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren; + if(idx.row() < cacheFirstRow || idx.row() >= cacheFirstRow + cache.size()) + return f; //Not fetched yet + + if(idx.column() == Import) + f.setFlag(Qt::ItemIsUserCheckable); + if(idx.column() == CustomName || idx.column() == MatchExisting) + f.setFlag(Qt::ItemIsEditable); + + return f; +} + +/* ISqlOnDemandModel */ + +void RSImportedModelsModel::clearCache() +{ + cache.clear(); + cache.squeeze(); + cacheFirstRow = 0; +} + +void RSImportedModelsModel::refreshData() +{ + if(!mDb.db()) + return; + + query q(mDb, "SELECT COUNT(1) FROM imported_rs_models"); + q.step(); + const int count = q.getRows().get(0); + if(count != totalItemsCount) //Invalidate cache and reset model + { + 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 RSImportedModelsModel::setSortingColumn(int col) +{ + if(sortColumn == col || col == CustomName || col == MatchExisting || col == SuffixCol || col == SubTypeCol) + return; //Do not sort by CustomName or MatchExisting (not useful and complicated) + + clearCache(); + sortColumn = col; + + QModelIndex first = index(0, 0); + QModelIndex last = index(curItemCount - 1, NCols - 1); + emit dataChanged(first, last); +} + +/* IRsImportModel */ + +int RSImportedModelsModel::countImported() +{ + query q(mDb, "SELECT COUNT(1) FROM imported_rs_models WHERE import=1"); + q.step(); + const int count = q.getRows().get(0); + return count; +} + +/* ICheckName */ + +bool RSImportedModelsModel::checkCustomNameValid(db_id importedModelId, const QString& originalName, const QString& newCustomName, QString *errTextOut) +{ + if(originalName == newCustomName) + { + if(errTextOut) + { + if(originalName.isEmpty()) + { + *errTextOut = tr("Models with empty name must have a Custom Name or must be matched to an existing model"); + }else{ + *errTextOut = tr("You cannot set the same name in the 'Custom Name' field.\n" + "If you meant to revert to original name then clear the custom name and leave the cell empty"); + } + } + return false; + } + + //FIXME: maybe use EXISTS instead of WHERE for performance + + //First check for duplicates + query q_nameDuplicates(mDb, "SELECT id FROM rs_models WHERE name=? LIMIT 1"); + + //If removing custom name check against original sheet name + QString nameToCheck = newCustomName; + if(newCustomName.isEmpty()) + nameToCheck = originalName; + + q_nameDuplicates.bind(1, nameToCheck); + + if(q_nameDuplicates.step() == SQLITE_ROW) + { + db_id modelId = q_nameDuplicates.getRows().get(0); + Q_UNUSED(modelId) //TODO: maybe use it? + + if(errTextOut) + { + *errTextOut = tr("There is already an existing Model with same name: %1\n" + "If you meant to merge theese rollingstock pieces with this existing model " + "please use 'Match Existing' field") + .arg(nameToCheck); + } + return false; + } + + //Check also against other imported models original names (that don't have a custom name) + q_nameDuplicates.prepare("SELECT id FROM imported_rs_models WHERE name=? AND new_name IS NULL AND id<>? LIMIT 1"); + q_nameDuplicates.bind(1, nameToCheck); + q_nameDuplicates.bind(2, importedModelId); + if(q_nameDuplicates.step() == SQLITE_ROW) + { + db_id modelId = q_nameDuplicates.getRows().get(0); + Q_UNUSED(modelId) //TODO: maybe use it? + + if(errTextOut) + { + *errTextOut = tr("There is already an imported Model with name: %1\n" + "If you meant to merge theese rollingstock pieces with this existing model " + "after importing rollingstock use the merge tool to merge them") + .arg(nameToCheck); + } + return false; + } + + //Check also against other imported models custom names + q_nameDuplicates.prepare("SELECT id, name FROM imported_rs_models WHERE new_name=? AND id<>? LIMIT 1"); + q_nameDuplicates.bind(1, nameToCheck); + q_nameDuplicates.bind(2, importedModelId); + if(q_nameDuplicates.step() == SQLITE_ROW) + { + db_id modelId = q_nameDuplicates.getRows().get(0); + Q_UNUSED(modelId) //TODO: maybe use it? + + QString otherOriginalName = q_nameDuplicates.getRows().get(1); + + if(newCustomName.isEmpty()) + { + if(errTextOut) + { + *errTextOut = tr("You already gave the same custom name: %1 " + "to the imported model: %2\n" + "In order to proceed you need to assign a different custom name to %2") + .arg(nameToCheck) + .arg(otherOriginalName); + } + } + else + { + if(errTextOut) + { + *errTextOut = tr("You already gave the same custom name: %1 " + "to the imported model: %2\n" + "Please choose a different name or leave empty for the original name") + .arg(nameToCheck) + .arg(otherOriginalName); + } + } + + return false; + } + + return true; +} + +/* IFKField */ + +bool RSImportedModelsModel::getFieldData(int row, int /*col*/, db_id &modelIdOut, QString &modelNameOut) const +{ + if(row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; + + const ModelItem& item = cache.at(row - cacheFirstRow); + modelIdOut = item.matchExistingId; + modelNameOut = item.matchExistingName; + + return true; +} + +bool RSImportedModelsModel::validateData(int /*row*/, int /*col*/, db_id /*modelId*/, const QString &/*modelName*/) +{ + return true; //TODO: implement +} + +bool RSImportedModelsModel::setFieldData(int row, int /*col*/, db_id modelId, const QString &modelName) +{ + //NOTE: CustomName and MatchExisting exclude each other + if(row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; + + ModelItem& item = cache[row - cacheFirstRow]; + if(item.matchExistingId == modelId) + return true; //No change + + if(modelId == 0) + { + //Check if we can leave it with no match and no custom name + //FIXME: if owner is matched and instead user wants to set a custom name + //but you can't remove the match because the name is a duplicate + //The user should set a custom name, it will automatically remove the match + //BUT we need to show a proper error test if just removing the match + //so add it to checkCustomNameValid() with like 'bool isMatchingOwner=true' argument + QString errText; + if(!checkCustomNameValid(item.importdModelId, item.name, QString(), &errText)) + { + emit modelError(errText); + return false; + } + } + + command set_match(mDb, "UPDATE imported_rs_models SET new_name=NULL,match_existing_id=? WHERE id=?"); + if(modelId) + set_match.bind(1, modelId); + else + set_match.bind(1); //Bind NULL + set_match.bind(2, item.importdModelId); + + if(set_match.execute() != SQLITE_OK) + return false; + + item.matchExistingId = modelId; + item.matchExistingName = modelName; + item.customName.clear(); + item.customName.squeeze(); + + emit dataChanged(index(row, CustomName), index(row, MatchExisting)); + + return true; +} diff --git a/src/rollingstock/importer/model/rsimportedmodelsmodel.h b/src/rollingstock/importer/model/rsimportedmodelsmodel.h new file mode 100644 index 0000000..e9c7d16 --- /dev/null +++ b/src/rollingstock/importer/model/rsimportedmodelsmodel.h @@ -0,0 +1,105 @@ +#ifndef RSIMPORTEDMODELSMODEL_H +#define RSIMPORTEDMODELSMODEL_H + +#include "rollingstock/importer/intefaces/irsimportmodel.h" +#include + +#include "utils/types.h" +#include "utils/sqldelegate/IFKField.h" + +#include +using namespace sqlite3pp; + +class RSImportedModelsModel : public IRsImportModel, public IFKField +{ + Q_OBJECT + +public: + enum { BatchSize = 100 }; + + enum Columns + { + Import = 0, + Name, + CustomName, + MatchExisting, + SuffixCol, + MaxSpeedCol, + AxesCol, + TypeCol, + SubTypeCol, + NCols + }; + + typedef struct + { + db_id importdModelId; + db_id matchExistingId; + QString name; + QString customName; + QString matchExistingName; + QString suffix; + qint16 maxSpeedKmH; + qint8 axes; + RsType type; + RsEngineSubType subType; + bool import; + } ModelItem; + + RSImportedModelsModel(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; + + // Editable: + bool setData(const QModelIndex &idx, const QVariant &value, + int role = Qt::EditRole) override; + + Qt::ItemFlags flags(const QModelIndex& idx) 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; + + + // IRsImportModel: + int countImported() override; + + + // ICheckName: + bool checkCustomNameValid(db_id importedModelId, const QString& originalName, const QString& newCustomName, + QString *errTextOut) override; + + // IFKField: + bool getFieldData(int row, int col, db_id &modelIdOut, QString &modelNameOut) const override; + bool validateData(int row, int col, db_id modelId, const QString &modelName) override; + bool setFieldData(int row, int col, db_id modelId, const QString &modelName) override; + +private: + void fetchRow(int row); + Q_INVOKABLE void internalFetch(int first, int sortCol, int valRow, const QVariant& val); + void handleResult(const QVector& items, int firstRow); + +private: + QVector cache; + int cacheFirstRow; + int firstPendingRow; +}; + +#endif // RSIMPORTEDMODELSMODEL_H diff --git a/src/rollingstock/importer/model/rsimportedownersmodel.cpp b/src/rollingstock/importer/model/rsimportedownersmodel.cpp new file mode 100644 index 0000000..70903bb --- /dev/null +++ b/src/rollingstock/importer/model/rsimportedownersmodel.cpp @@ -0,0 +1,761 @@ +#include "rsimportedownersmodel.h" + +#include "../rsimportstrings.h" + +#include "utils/model_roles.h" + +#include +#include "utils/worker_event_types.h" + +#include + +#include + +#include + +class OwnerResultEvent : public QEvent +{ +public: + static constexpr Type _Type = Type(CustomEvents::RsImportedOwnersResult); + inline OwnerResultEvent() : QEvent(_Type) {} + + QVector items; + int firstRow; +}; + +RSImportedOwnersModel::RSImportedOwnersModel(database &db, QObject *parent) : + IRsImportModel(db, parent), + cacheFirstRow(0), + firstPendingRow(-BatchSize) +{ + sortColumn = SheetIdx; +} + +bool RSImportedOwnersModel::event(QEvent *e) +{ + if(e->type() == OwnerResultEvent::_Type) + { + OwnerResultEvent *ev = static_cast(e); + ev->setAccepted(true); + + handleResult(ev->items, ev->firstRow); + + return true; + } + + return QAbstractTableModel::event(e); +} + +void RSImportedOwnersModel::fetchRow(int row) +{ + if(row >= firstPendingRow && row < firstPendingRow + BatchSize) + return; //Already fetching + + 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; + + //NOTE: Sorting hint: + //because LIMIT ? OFFSET ? can be slow when offset is big + //we give an hint to reduce offset + QVariant val; + int valRow = 0; + OwnerItem *item = nullptr; + + //We can give an hint only if we already have cached some data + 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 (sortColumn) + { + case SheetIdx: + { + if(item) + { + val = item->sheetIdx; + } + break; + } + case Name: + { + if(item) + { + val = item->name; + } + break; + } + //No data hint for Import column + } + + //TODO: use a custom QRunnable + QMetaObject::invokeMethod(this, "internalFetch", Qt::QueuedConnection, + Q_ARG(int, firstPendingRow), Q_ARG(int, sortColumn), + Q_ARG(int, valRow), Q_ARG(QVariant, val)); +} + +void RSImportedOwnersModel::internalFetch(int first, int sortCol, int valRow, const QVariant& val) +{ + query q(mDb); + + int offset = first - valRow; + 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 imp.id,imp.name,imp.import,imp.new_name,imp.match_existing_id,imp.sheet_idx,rs_owners.name" + " FROM imported_rs_owners imp LEFT JOIN rs_owners ON rs_owners.id=imp.match_existing_id"; + switch (sortCol) + { + case SheetIdx: + { + whereCol = "imp.sheet_idx"; + break; + } + case Name: + { + whereCol = "imp.name"; + break; + } + case Import: + { + whereCol = "imp.import DESC, imp.name"; //Order by 2 columns, no where clause + break; + } + } + + if(val.isValid()) + { + sql += " WHERE "; + sql += whereCol; + if(reverse) + 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); + + if(val.isValid()) + { + switch (sortCol) + { + case SheetIdx: + { + q.bind(3, val.toInt()); + break; + } + 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; + OwnerItem &item = vec[i]; + item.importedOwnerId = r.get(0); + item.name = r.get(1); + item.import = r.get(2) != 0; + if(r.column_type(3) != SQLITE_NULL) + item.customName = r.get(3); + item.matchExistingId = r.get(4); + item.sheetIdx = r.get(5); + if(item.matchExistingId) + item.matchExistingName = r.get(6); + i--; + } + if(i > -1) + vec.remove(0, i + 1); + } + else + { + int i = 0; + + for(; it != end; ++it) + { + auto r = *it; + OwnerItem &item = vec[i]; + item.importedOwnerId = r.get(0); + item.name = r.get(1); + item.import = r.get(2) != 0; + if(r.column_type(3) != SQLITE_NULL) + item.customName = r.get(3); + item.matchExistingId = r.get(4); + item.sheetIdx = r.get(5); + if(item.matchExistingId) + item.matchExistingName = r.get(6); + i++; + } + if(i < BatchSize) + vec.remove(i, BatchSize - i); + } + + OwnerResultEvent *ev = new OwnerResultEvent; + ev->items = vec; + ev->firstRow = first; + + qApp->postEvent(this, ev); +} + +void RSImportedOwnersModel::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; + + QModelIndex firstIdx = index(firstRow, 0); + QModelIndex lastIdx = index(firstRow + items.count() - 1, NCols - 1); + emit dataChanged(firstIdx, lastIdx); + + qDebug() << "TOTAL: From:" << cacheFirstRow << "To:" << cacheFirstRow + cache.size() - 1; +} + +/* QAbstractTableModel: */ + +QVariant RSImportedOwnersModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch (section) + { + case SheetIdx: + return RsImportStrings::tr("Sheet No."); + case Import: + return RsImportStrings::tr("Import"); + case Name: + return RsImportStrings::tr("Name"); + case CustomName: + return RsImportStrings::tr("Custom Name"); + case MatchExisting: + return RsImportStrings::tr("Match Existing"); + } + } + return QAbstractTableModel::headerData(section, orientation, role); +} + +int RSImportedOwnersModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : curItemCount; +} + +int RSImportedOwnersModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant RSImportedOwnersModel::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 QVariant(); + } + + auto item = cache.at(row - cacheFirstRow); + + switch (role) + { + case Qt::DisplayRole: + { + switch (idx.column()) + { + case SheetIdx: + return item.sheetIdx + 1; //1-based index (Start from 1) + case Name: + return item.name; + case CustomName: + return item.customName; + case MatchExisting: + return item.matchExistingName; + } + break; + } + case Qt::EditRole: + { + switch (idx.column()) + { + case CustomName: + return item.customName; + } + break; + } + case Qt::BackgroundRole: + { + if(!item.import || + (idx.column() == CustomName && item.customName.isEmpty()) || + (idx.column() == MatchExisting && item.matchExistingId == 0)) + return QBrush(Qt::lightGray); //If not imported mark background or no custom/matching name set + break; + } + case Qt::CheckStateRole: + { + switch (idx.column()) + { + case Import: + return item.import ? Qt::Checked : Qt::Unchecked; + } + break; + } + } + + return QVariant(); +} + +bool RSImportedOwnersModel::setData(const QModelIndex &idx, const QVariant &value, int role) +{ + const int row = idx.row(); + if(!idx.isValid() || row >= curItemCount || idx.column() >= NCols || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + OwnerItem &item = cache[row - cacheFirstRow]; + QModelIndex first = idx; + QModelIndex last = idx; + + switch (role) + { + case Qt::EditRole: + { + switch (idx.column()) + { + case CustomName: + { + const QString newName = value.toString().simplified(); + if(item.customName == newName) + return false; //No change + + QString errText; + if(!checkCustomNameValid(item.importedOwnerId, item.name, newName, &errText)) + { + emit modelError(errText); + return false; + } + + command set_name(mDb, "UPDATE imported_rs_owners SET new_name=?,match_existing_id=NULL WHERE id=?"); + + if(newName.isEmpty()) + set_name.bind(1); //Bind NULL + else + set_name.bind(1, newName); + set_name.bind(2, item.importedOwnerId); + if(set_name.execute() != SQLITE_OK) + return false; + + item.customName = newName; + item.matchExistingId = 0; + item.matchExistingName.clear(); + item.matchExistingName.squeeze(); + + last = index(row, MatchExisting); + + break; + } + default: + return false; + } + break; + } + case Qt::CheckStateRole: + { + switch (idx.column()) + { + case Import: + { + Qt::CheckState cs = value.value(); + const bool import = cs == Qt::Checked; + if(item.import == import) + return false; //No change + + if(import) + { + //Newly imported, check if there are duplicates + QString errText; + if(!checkCustomNameValid(item.importedOwnerId, item.name, item.customName, &errText)) + { + emit modelError(errText); + return false; + } + } + + command set_imported(mDb, "UPDATE imported_rs_owners SET import=? WHERE id=?"); + set_imported.bind(1, import ? 1 : 0); + set_imported.bind(2, item.importedOwnerId); + if(set_imported.execute() != SQLITE_OK) + return false; + + item.import = import; + + if(sortColumn == Import) + { + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + } + + emit importCountChanged(); + + //Update all columns to update background + first = index(row, 0); + last = index(row, NCols - 1); + break; + } + default: + return false; + } + break; + } + default: + return false; + } + + emit dataChanged(first, last); + return true; +} + +Qt::ItemFlags RSImportedOwnersModel::flags(const QModelIndex &idx) const +{ + if (!idx.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren; + if(idx.row() < cacheFirstRow || idx.row() >= cacheFirstRow + cache.size()) + return f; //Not fetched yet + + if(idx.column() == Import) + f.setFlag(Qt::ItemIsUserCheckable); + if(idx.column() == CustomName || idx.column() == MatchExisting) + f.setFlag(Qt::ItemIsEditable); + + return f; +} + +/* ISqlOnDemandModel */ + +void RSImportedOwnersModel::clearCache() +{ + cache.clear(); + cache.squeeze(); + cacheFirstRow = 0; +} + +void RSImportedOwnersModel::refreshData() +{ + if(!mDb.db()) + return; + + query q(mDb, "SELECT COUNT(1) FROM imported_rs_owners"); + q.step(); + const int count = q.getRows().get(0); + if(count != totalItemsCount) //Invalidate cache and reset model + { + 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 RSImportedOwnersModel::setSortingColumn(int col) +{ + if(sortColumn == col || col == CustomName || col == MatchExisting) + return; //Do not sort by CustomName or MatchExisting (not useful and complicated) + + clearCache(); + sortColumn = col; + + QModelIndex first = index(0, 0); + QModelIndex last = index(curItemCount - 1, NCols - 1); + emit dataChanged(first, last); +} + +/* IRsImportModel */ + +int RSImportedOwnersModel::countImported() +{ + query q(mDb, "SELECT COUNT(1) FROM imported_rs_owners WHERE import=1"); + q.step(); + const int count = q.getRows().get(0); + return count; +} + +/* ICheckName */ + +bool RSImportedOwnersModel::checkCustomNameValid(db_id importedOwnerId, const QString& originalName, const QString& newCustomName, QString *errTextOut) +{ + if(originalName == newCustomName) + { + if(errTextOut) + { + if(originalName.isEmpty()) + { + *errTextOut = tr("Owners with empty name must have a Custom Name or must be matched to an existing owner"); + }else{ + *errTextOut = tr("You cannot set the same name in the 'Custom Name' field.\n" + "If you meant to revert to original name then clear the custom name and leave the cell empty"); + } + } + return false; + } + + //FIXME: maybe use EXISTS instead of WHERE for performance + + //First check for duplicates + query q_nameDuplicates(mDb, "SELECT id FROM rs_owners WHERE name=? LIMIT 1"); + + //If removing custom name check against original sheet name + QString nameToCheck = newCustomName; + if(newCustomName.isEmpty()) + nameToCheck = originalName; + + q_nameDuplicates.bind(1, nameToCheck); + + if(q_nameDuplicates.step() == SQLITE_ROW) + { + db_id ownerId = q_nameDuplicates.getRows().get(0); + Q_UNUSED(ownerId) //TODO: maybe use it? + + if(errTextOut) + { + *errTextOut = tr("There is already an existing Owner with same name: %1\n" + "If you meant to merge theese rollingstock pieces with this existing owner " + "please use 'Match Existing' field") + .arg(nameToCheck); + } + return false; + } + + //Check also against other imported owners original names (that don't have a custom name) + q_nameDuplicates.prepare("SELECT id FROM imported_rs_owners WHERE name=? AND new_name IS NULL AND id<>? LIMIT 1"); + q_nameDuplicates.bind(1, nameToCheck); + q_nameDuplicates.bind(2, importedOwnerId); + if(q_nameDuplicates.step() == SQLITE_ROW) + { + db_id ownerId = q_nameDuplicates.getRows().get(0); + Q_UNUSED(ownerId) //TODO: maybe use it? + + if(errTextOut) + { + *errTextOut = tr("There is already an imported Owner with name: %1\n" + "If you meant to merge theese rollingstock pieces with this existing owner " + "after importing rollingstock use the merge tool to merge them") + .arg(nameToCheck); + } + return false; + } + + //Check also against other imported owners custom names + q_nameDuplicates.prepare("SELECT id, name FROM imported_rs_owners WHERE new_name=? AND id<>? LIMIT 1"); + q_nameDuplicates.bind(1, nameToCheck); + q_nameDuplicates.bind(2, importedOwnerId); + if(q_nameDuplicates.step() == SQLITE_ROW) + { + auto r = q_nameDuplicates.getRows(); + db_id ownerId = r.get(0); + Q_UNUSED(ownerId) //TODO: maybe use it? + + QString otherOriginalName = r.get(1); + + if(newCustomName.isEmpty()) + { + if(errTextOut) + { + *errTextOut = tr("You already gave the same custom name: %1 " + "to the imported owner: %2\n" + "In order to proceed you need to assign a different custom name to %2") + .arg(nameToCheck) + .arg(otherOriginalName); + } + } + else + { + if(errTextOut) + { + *errTextOut = tr("You already gave the same custom name: %1 " + "to the imported owner: %2\n" + "Please choose a different name or leave empty for the original name") + .arg(nameToCheck) + .arg(otherOriginalName); + } + } + + return false; + } + + return true; +} + +/* IFKField */ + +bool RSImportedOwnersModel::getFieldData(int row, int /*col*/, db_id &ownerIdOut, QString &ownerNameOut) const +{ + if(row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; + + const OwnerItem& item = cache.at(row - cacheFirstRow); + ownerIdOut = item.matchExistingId; + ownerNameOut = item.matchExistingName; + + return true; +} + +bool RSImportedOwnersModel::validateData(int /*row*/, int /*col*/, db_id /*ownerId*/, const QString &/*ownerName*/) +{ + return true; //TODO: implement +} + +bool RSImportedOwnersModel::setFieldData(int row, int /*col*/, db_id ownerId, const QString &ownerName) +{ + //NOTE: CustomName and MatchExisting exclude each other + if(row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; + + OwnerItem& item = cache[row - cacheFirstRow]; + if(item.matchExistingId == ownerId) + return true; //No change + + if(ownerId == 0) + { + //Check if we can leave it with no match and no custom name + //FIXME: if owner is matched and instead user wants to set a custom name + //but you can't remove the match because the name is a duplicate + //The user should set a custom name, it will automatically remove the match + //BUT we need to show a proper error test if just removing the match + //so add it to checkCustomNameValid() with like 'bool isMatchingOwner=true' argument + QString errText; + if(!checkCustomNameValid(item.importedOwnerId, item.name, QString(), &errText)) + { + emit modelError(errText); + return false; + } + } + + command set_match(mDb, "UPDATE imported_rs_owners SET new_name=NULL,match_existing_id=? WHERE id=?"); + if(ownerId) + set_match.bind(1, ownerId); + else + set_match.bind(1); //Bind NULL + set_match.bind(2, item.importedOwnerId); + + if(set_match.execute() != SQLITE_OK) + return false; + + item.matchExistingId = ownerId; + item.matchExistingName = ownerName; + item.customName.clear(); + item.customName.squeeze(); + + emit dataChanged(index(row, CustomName), index(row, MatchExisting)); + + return true; +} + diff --git a/src/rollingstock/importer/model/rsimportedownersmodel.h b/src/rollingstock/importer/model/rsimportedownersmodel.h new file mode 100644 index 0000000..25f135d --- /dev/null +++ b/src/rollingstock/importer/model/rsimportedownersmodel.h @@ -0,0 +1,96 @@ +#ifndef RSIMPORTEDOWNERSMODEL_H +#define RSIMPORTEDOWNERSMODEL_H + +#include "rollingstock/importer/intefaces/irsimportmodel.h" +#include + +#include "utils/types.h" +#include "utils/sqldelegate/IFKField.h" + +#include +using namespace sqlite3pp; + +class RSImportedOwnersModel : public IRsImportModel, public IFKField +{ + Q_OBJECT + +public: + enum { BatchSize = 100 }; + + enum Columns + { + SheetIdx = 0, + Import, + Name, + CustomName, + MatchExisting, + NCols + }; + + typedef struct + { + db_id importedOwnerId; + db_id matchExistingId; + QString name; + QString customName; + QString matchExistingName; + int sheetIdx; + bool import; + } OwnerItem; + + RSImportedOwnersModel(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; + + // Editable: + bool setData(const QModelIndex &idx, const QVariant &value, + int role = Qt::EditRole) override; + + Qt::ItemFlags flags(const QModelIndex& idx) 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; + + + // IRsImportModel: + int countImported() override; + + + // ICheckName: + bool checkCustomNameValid(db_id importedOwnerId, const QString& originalName, const QString& newCustomName, + QString *errTextOut) override; + + // IFKField: + bool getFieldData(int row, int col, db_id &ownerIdOut, QString &ownerNameOut) const override; + bool validateData(int row, int col, db_id ownerId, const QString &ownerName) override; + bool setFieldData(int row, int col, db_id ownerId, const QString &ownerName) override; + +private: + void fetchRow(int row); + Q_INVOKABLE void internalFetch(int first, int sortCol, int valRow, const QVariant& val); + void handleResult(const QVector& items, int firstRow); + +private: + QVector cache; + int cacheFirstRow; + int firstPendingRow; +}; + +#endif // RSIMPORTEDOWNERSMODEL_H diff --git a/src/rollingstock/importer/model/rsimportedrollingstockmodel.cpp b/src/rollingstock/importer/model/rsimportedrollingstockmodel.cpp new file mode 100644 index 0000000..9b19632 --- /dev/null +++ b/src/rollingstock/importer/model/rsimportedrollingstockmodel.cpp @@ -0,0 +1,773 @@ +#include "rsimportedrollingstockmodel.h" + +#include "../rsimportstrings.h" + +#include "utils/rs_utils.h" + +#include +#include "utils/worker_event_types.h" + +#include + +#include + +#include + +class RSResultEvent : public QEvent +{ +public: + static constexpr Type _Type = Type(CustomEvents::RsImportedRSModelResult); + inline RSResultEvent() : QEvent(_Type) {} + + QVector items; + int firstRow; +}; + +RSImportedRollingstockModel::RSImportedRollingstockModel(database &db, QObject *parent) : + IRsImportModel(db, parent), + cacheFirstRow(0), + firstPendingRow(-BatchSize) +{ + sortColumn = Owner; +} + +bool RSImportedRollingstockModel::event(QEvent *e) +{ + if(e->type() == RSResultEvent::_Type) + { + RSResultEvent *ev = static_cast(e); + ev->setAccepted(true); + + handleResult(ev->items, ev->firstRow); + + return true; + } + + return QAbstractTableModel::event(e); +} + +void RSImportedRollingstockModel::fetchRow(int row) +{ + if(row >= firstPendingRow && row < firstPendingRow + BatchSize) + return; //Already fetching + + 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; + 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; //It's shortet to get here by reverse from cache first + item = &cache.first(); + } + } + + switch (sortColumn) + { + case Model: + { + if(item) + { + val = item->modelName; + } + break; + } + case Number: + { + if(item) + { + val = item->number; + } + break; + } + case Owner: + { + if(item) + { + val = item->ownerName; + } + break; + } + } + + //TODO: use a custom QRunnable + QMetaObject::invokeMethod(this, "internalFetch", Qt::QueuedConnection, + Q_ARG(int, firstPendingRow), Q_ARG(int, sortColumn), + Q_ARG(int, valRow), Q_ARG(QVariant, val)); +} + +void RSImportedRollingstockModel::internalFetch(int first, int sortCol, int valRow, const QVariant& val) +{ + query q(mDb); + + int offset = first - valRow; + 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 imp.id, imp.import, imp.model_id, imp.owner_id, imp.number, imp.new_number, models.name, models.type, owners.name, owners.new_name" + " FROM imported_rs_list imp" + " JOIN imported_rs_models models ON models.id=imp.model_id" + " JOIN imported_rs_owners owners ON owners.id=imp.owner_id"; + switch (sortCol) + { + case Import: + { + whereCol = "imp.import"; + break; + } + case Model: + { + whereCol = "models.name"; + break; + } + case Number: + { + whereCol = "imp.number"; + break; + } + case Owner: + { + whereCol = "owners.name"; + break; + } + } + + sql += " WHERE owners.import=1 AND models.import=1"; + if(val.isValid()) + { + sql += " AND "; + sql += whereCol; + if(reverse) + sql += "?3"; + } + + sql += " ORDER BY "; + sql += whereCol; + + if(reverse) + sql += " DESC"; + + sql += " LIMIT ?1"; + if(offset) + sql += " OFFSET ?2"; + + q.prepare(sql); + sqlite3_stmt *st = q.stmt(); + q.bind(1, BatchSize); + if(offset) + q.bind(2, offset); + + if(val.isValid()) + { + switch (sortCol) + { + case Model: + case Owner: + { + QByteArray name = val.toByteArray(); + sqlite3_bind_text(st, 3, name, name.size(), SQLITE_TRANSIENT); + break; + } + case Number: + { + int num = val.toInt(); + q.bind(3, num); + 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.importdRsId = r.get(0); + item.import = r.get(1) == 1; + item.importedModelId = r.get(2); + item.importedOwnerId = r.get(3); + + item.number = r.get(4); + + if(r.column_type(5) == SQLITE_NULL) + item.new_number = -1; + else + item.new_number = r.get(5); + + item.modelName = QByteArray(reinterpret_cast(sqlite3_column_text(st, 6)), + sqlite3_column_bytes(st, 6)); + item.type = RsType(r.get(7)); + + item.ownerName = QByteArray(reinterpret_cast(sqlite3_column_text(st, 8)), + sqlite3_column_bytes(st, 8)); + + if(r.column_type(8) != SQLITE_NULL) + { + item.ownerCustomName = QByteArray(reinterpret_cast(sqlite3_column_text(st, 9)), + sqlite3_column_bytes(st, 9)); + } + 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.importdRsId = r.get(0); + item.import = r.get(1) == 1; + item.importedModelId = r.get(2); + item.importedOwnerId = r.get(3); + + item.number = r.get(4); + + if(r.column_type(5) == SQLITE_NULL) + item.new_number = -1; + else + item.new_number = r.get(5); + + item.modelName = QByteArray(reinterpret_cast(sqlite3_column_text(st, 6)), + sqlite3_column_bytes(st, 6)); + item.type = RsType(r.get(7)); + + item.ownerName = QByteArray(reinterpret_cast(sqlite3_column_text(st, 8)), + sqlite3_column_bytes(st, 8)); + + if(r.column_type(8) != SQLITE_NULL) + { + item.ownerCustomName = QByteArray(reinterpret_cast(sqlite3_column_text(st, 9)), + sqlite3_column_bytes(st, 9)); + } + i++; + } + if(i < BatchSize) + vec.remove(i, BatchSize - i); + } + + + RSResultEvent *ev = new RSResultEvent; + ev->items = vec; + ev->firstRow = first; + + qApp->postEvent(this, ev); +} + +void RSImportedRollingstockModel::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; + + QModelIndex firstIdx = index(firstRow, 0); + QModelIndex lastIdx = index(firstRow + items.count() - 1, NCols - 1); + emit dataChanged(firstIdx, lastIdx); + + qDebug() << "TOTAL: From:" << cacheFirstRow << "To:" << cacheFirstRow + cache.size() - 1; +} + +/* QAbstractTableModel */ + +QVariant RSImportedRollingstockModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch (section) + { + case Import: + return RsImportStrings::tr("Import"); + case Model: + return RsImportStrings::tr("Model"); + case Number: + return RsImportStrings::tr("Number"); + case Owner: + return RsImportStrings::tr("Owner"); + case NewNumber: + return RsImportStrings::tr("New number"); + } + } + return QAbstractTableModel::headerData(section, orientation, role); +} + +int RSImportedRollingstockModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : curItemCount; +} + +int RSImportedRollingstockModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant RSImportedRollingstockModel::data(const QModelIndex &idx, int role) 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 QVariant("..."); + } + + auto item = cache.at(row - cacheFirstRow); + + switch (role) + { + case Qt::DisplayRole: + { + switch (idx.column()) + { + case Model: + return item.modelName; + case Number: + return rs_utils::formatNum(item.type, item.number); + case Owner: + if(item.ownerCustomName.isEmpty()) + return item.ownerName; + return item.ownerName + " (" + item.ownerCustomName + ')'; + case NewNumber: + return item.new_number == -1 ? QVariant() : rs_utils::formatNum(item.type, item.new_number); + } + break; + } + case Qt::EditRole: + { + switch (idx.column()) + { + case NewNumber: + return item.new_number; + } + break; + } + case Qt::TextAlignmentRole: + { + if(idx.column() == Number || idx.column() == NewNumber) + return Qt::AlignRight + Qt::AlignVCenter; + break; + } + case Qt::BackgroundRole: + { + if(!item.import || (idx.column() == NewNumber && item.new_number == -1)) + return QBrush(Qt::lightGray); //If not imported mark background or no custom number set + break; + } + case Qt::CheckStateRole: + { + switch (idx.column()) + { + case Import: + return item.import ? Qt::Checked : Qt::Unchecked; + case NewNumber: + if(item.new_number == -1) + return QVariant(); + return Qt::Checked; + } + break; + } + } + + return QVariant(); +} + +bool RSImportedRollingstockModel::setData(const QModelIndex &idx, const QVariant &value, int role) +{ + const int row = idx.row(); + if(!idx.isValid() || row >= curItemCount || idx.column() >= NCols || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + RSItem &item = cache[row - cacheFirstRow]; + + QModelIndex first = idx; + QModelIndex last = idx; + + switch (role) + { + case Qt::EditRole: + { + switch (idx.column()) + { + case NewNumber: + { + int newNumber = value.toInt() % 10000; + + if(item.new_number == newNumber) + return false; + + QString errText; + if(!checkNewNumberIsValid(item.importdRsId, item.importedModelId, item.importedModelMatchId, + item.type, item.number, newNumber, &errText)) + { + emit modelError(errText); + return false; + } + + command set_newNumber(mDb, "UPDATE imported_rs_list SET new_number=? WHERE id=?"); + if(newNumber == -1) + set_newNumber.bind(1); //Bind NULL + else + set_newNumber.bind(1, newNumber); + set_newNumber.bind(2, item.importdRsId); + if(set_newNumber.execute() != SQLITE_OK) + return false; + + item.new_number = newNumber; + + break; + } + default: + return false; + } + break; + } + case Qt::CheckStateRole: + { + switch (idx.column()) + { + case Import: + { + Qt::CheckState cs = value.value(); + const bool import = cs == Qt::Checked; + if(item.import == import) + return false; //No change + + if(import) + { + //Newly imported, check if there are duplicates + QString errText; + if(!checkNewNumberIsValid(item.importdRsId, item.importedModelId, item.importedModelMatchId, + item.type, item.number, item.new_number, &errText)) + { + emit modelError(errText); + return false; + } + } + + command set_imported(mDb, "UPDATE imported_rs_list SET import=? WHERE id=?"); + set_imported.bind(1, import ? 1 : 0); + set_imported.bind(2, item.importdRsId); + if(set_imported.execute() != SQLITE_OK) + return false; + + item.import = import; + + if(sortColumn == Import) + { + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + } + + emit importCountChanged(); + + //Update all columns to update background + first = index(row, 0); + last = index(row, NCols - 1); + break; + } + case NewNumber: + { + Qt::CheckState cs = value.value(); + if(cs == Qt::Unchecked) + return setData(idx, -1, Qt::EditRole); //Set -1 as new_number -> NULL + return false; + } + default: + return false; + } + break; + } + default: + return false; + } + + emit dataChanged(first, last); + return true; +} + +Qt::ItemFlags RSImportedRollingstockModel::flags(const QModelIndex &idx) const +{ + if (!idx.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren; + if(idx.row() < cacheFirstRow || idx.row() >= cacheFirstRow + cache.size()) + return f; //Not fetched yet + + if(idx.column() == Import) + f.setFlag(Qt::ItemIsUserCheckable); + if(idx.column() == NewNumber) + { + f.setFlag(Qt::ItemIsEditable); + f.setFlag(Qt::ItemIsUserCheckable, cache.at(idx.row() - cacheFirstRow).new_number != -1); + } + + return f; +} + +/* ISqlOnDemandModel */ + +void RSImportedRollingstockModel::clearCache() +{ + cache.clear(); + cache.squeeze(); + cacheFirstRow = 0; +} + +void RSImportedRollingstockModel::refreshData() +{ + query q(mDb, "SELECT COUNT(1) FROM imported_rs_list imp" + " JOIN imported_rs_models m ON m.id=imp.model_id" + " JOIN imported_rs_owners o ON o.id=imp.owner_id" + " WHERE o.import=1 AND m.import=1"); + q.step(); + const int count = q.getRows().get(0); + if(count != totalItemsCount) //Invalidate cache and reset model + { + 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 RSImportedRollingstockModel::setSortingColumn(int col) +{ + if(sortColumn == col || col == NewNumber) + return; //Don't sort by NewNumber because some are NULL + + clearCache(); + sortColumn = col; + + QModelIndex first = index(0, 0); + QModelIndex last = index(curItemCount - 1, NCols - 1); + emit dataChanged(first, last); +} + +/* IRsImportModel */ + +int RSImportedRollingstockModel::countImported() +{ + query q(mDb, "SELECT COUNT(1) FROM imported_rs_list imp" + " JOIN imported_rs_models m ON m.id=imp.model_id" + " JOIN imported_rs_owners o ON o.id=imp.owner_id" + " WHERE imp.import=1 AND o.import=1 AND m.import=1"); + q.step(); + const int count = q.getRows().get(0); + return count; +} + +/* ICheckName */ + +bool RSImportedRollingstockModel::checkNewNumberIsValid(db_id importedRsId, db_id importedModelId, db_id matchExistingModelId, + RsType rsType, int number, int newNumber, QString *errTextOut) +{ + RsType type = RsType(rsType); + + if(number == newNumber) + { + if(errTextOut) + { + *errTextOut = tr("You cannot set the same name in the 'Custom Name' field.\n" + "If you meant to revert to original name then clear the custom name and leave the cell empty"); + } + return false; + } + + int numberToCheck = newNumber; + if(newNumber == -1) + numberToCheck = number; + + //First check if there is an imported RS with same number or new number + query q(mDb, "SELECT imp.id, imp.new_number, m.name, m.new_name, rs_models.name" + " FROM imported_rs_models m" + " LEFT JOIN rs_models ON rs_models.id=m.match_existing_id" + " JOIN imported_rs_list imp ON imp.model_id=m.id" + " JOIN imported_rs_owners own ON own.id=imp.owner_id" + " WHERE (m.id=?1 OR (m.match_existing_id=?2 AND m.match_existing_id NOT NULL))" + " AND imp.id<>?3 AND imp.import=1 AND m.import=1 AND own.import=1" + " AND ((imp.new_number IS NULL AND imp.number=?4) OR imp.new_number=?4)"); + q.bind(1, importedModelId); + q.bind(2, matchExistingModelId); + q.bind(3, importedRsId); + q.bind(4, numberToCheck); + + if(q.step() == SQLITE_ROW) + { + auto r = q.getRows(); + db_id dupId = r.get(0); + Q_UNUSED(dupId) //TODO: maybe use it? + + sqlite3_stmt *st = q.stmt(); + + bool matchedNewNumber = true; + + if(r.column_type(1) == SQLITE_NULL) + { + matchedNewNumber = false; //We matched original number + } + + QByteArray modelName = QByteArray(reinterpret_cast(sqlite3_column_text(st, 2)), + sqlite3_column_bytes(st, 2)); + + if(errTextOut) + { + if(r.column_type(3) != SQLITE_NULL) + { + // 'name (new_name)' + modelName.append(" (", 2); + modelName.append(reinterpret_cast(sqlite3_column_text(st, 3)), + sqlite3_column_bytes(st, 3)); + modelName.append(')'); + } + + if(r.column_type(4) != SQLITE_NULL) + { + // 'name (match_existing name)' + modelName.append(" (", 2); + modelName.append(reinterpret_cast(sqlite3_column_text(st, 4)), + sqlite3_column_bytes(st, 4)); + modelName.append(')'); + } + + QString model = QString::fromUtf8(modelName); + + if(matchedNewNumber) + { + *errTextOut = tr("There is already another imported rollingstock with same 'New Number': %1 %2") + .arg(model).arg(rs_utils::formatNum(type, numberToCheck)); + }else{ + *errTextOut = tr("There is already another imported rollingstock with same number: %1 %2") + .arg(model).arg(rs_utils::formatNum(type, numberToCheck)); + } + } + return false; + } + + //Then check for an existing RS with same number if model is matched + if(matchExistingModelId) + { + q.prepare("SELECT rs_list.id, rs_models.name" + " FROM rs_list" + " JOIN rs_models ON rs_models.id=?1" + " WHERE rs_list.model_id=?1 AND rs_list.number=?"); + q.bind(1, matchExistingModelId); + q.bind(2, numberToCheck); + + if(q.step() == SQLITE_ROW) + { + auto r = q.getRows(); + db_id dupExistingId = r.get(0); + Q_UNUSED(dupExistingId) //TODO: maybe use it? + + QString modelName = r.get(1); + + if(errTextOut) + { + *errTextOut = tr("There is already an existing rollingstock with same number: %1 %2") + .arg(modelName).arg(rs_utils::formatNum(type, numberToCheck)); + } + return false; + } + } + + return true; +} diff --git a/src/rollingstock/importer/model/rsimportedrollingstockmodel.h b/src/rollingstock/importer/model/rsimportedrollingstockmodel.h new file mode 100644 index 0000000..31048a6 --- /dev/null +++ b/src/rollingstock/importer/model/rsimportedrollingstockmodel.h @@ -0,0 +1,95 @@ +#ifndef RSIMPORTEDROLLINGSTOCKMODEL_H +#define RSIMPORTEDROLLINGSTOCKMODEL_H + +#include "rollingstock/importer/intefaces/irsimportmodel.h" +#include + +#include "utils/types.h" + +#include +using namespace sqlite3pp; + +class RSImportedRollingstockModel : public IRsImportModel +{ + Q_OBJECT + +public: + enum { BatchSize = 100 }; + + enum Columns + { + Import = 0, + Model, + Number, + Owner, + NewNumber, + NCols + }; + + typedef struct + { + db_id importdRsId; + db_id importedModelId; + db_id importedModelMatchId; + db_id importedOwnerId; + int number; + int new_number; + QByteArray modelName; + QByteArray ownerName; + QByteArray ownerCustomName; + bool import; + RsType type; + } RSItem; + + RSImportedRollingstockModel(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; + + // Editable: + bool setData(const QModelIndex &idx, const QVariant &value, + int role = Qt::EditRole) override; + + Qt::ItemFlags flags(const QModelIndex& idx) 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; + + + // IRsImportModel: + int countImported() override; + + + // ICheckName: + bool checkNewNumberIsValid(db_id importedRsId, db_id importedModelId, db_id matchExistingModelId, RsType rsType, + int number, int newNumber, + QString *errTextOut) override; + +private: + void fetchRow(int row); + Q_INVOKABLE void internalFetch(int first, int sortCol, int valRow, const QVariant& val); + void handleResult(const QVector& items, int firstRow); + +private: + QVector cache; + int cacheFirstRow; + int firstPendingRow; +}; + +#endif // RSIMPORTEDROLLINGSTOCKMODEL_H diff --git a/src/rollingstock/importer/pages/CMakeLists.txt b/src/rollingstock/importer/pages/CMakeLists.txt new file mode 100644 index 0000000..375469b --- /dev/null +++ b/src/rollingstock/importer/pages/CMakeLists.txt @@ -0,0 +1,15 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + rollingstock/importer/pages/choosefilepage.h + rollingstock/importer/pages/fixduplicatesdlg.h + rollingstock/importer/pages/itemselectionpage.h + rollingstock/importer/pages/loadingpage.h + rollingstock/importer/pages/optionspage.h + + rollingstock/importer/pages/choosefilepage.cpp + rollingstock/importer/pages/fixduplicatesdlg.cpp + rollingstock/importer/pages/itemselectionpage.cpp + rollingstock/importer/pages/loadingpage.cpp + rollingstock/importer/pages/optionspage.cpp + PARENT_SCOPE +) diff --git a/src/rollingstock/importer/pages/choosefilepage.cpp b/src/rollingstock/importer/pages/choosefilepage.cpp new file mode 100644 index 0000000..23d3859 --- /dev/null +++ b/src/rollingstock/importer/pages/choosefilepage.cpp @@ -0,0 +1,107 @@ +#include "choosefilepage.h" + +#include +#include +#include + +#include +#include + +#include + +#include "../rsimportwizard.h" +#include "../rsimportstrings.h" + +#include "utils/file_format_names.h" + +ChooseFilePage::ChooseFilePage(QWidget *parent) : + QWizardPage(parent) +{ + QHBoxLayout *lay = new QHBoxLayout(this); + + pathEdit = new QLineEdit; + pathEdit->setPlaceholderText(RsImportStrings::tr("Insert path here or click 'Choose' button")); + connect(pathEdit, &QLineEdit::textChanged, this, &QWizardPage::completeChanged); + lay->addWidget(pathEdit); + + chooseBut = new QPushButton(RsImportStrings::tr("Choose")); + connect(chooseBut, &QPushButton::clicked, this, &ChooseFilePage::onChoose); + lay->addWidget(chooseBut); + + setTitle(RsImportStrings::tr("Choose file")); + setSubTitle(RsImportStrings::tr("Choose a file to import in *.ods format")); + + //Prevent user from going back to this page and change file. + //If user wants to change file he has to Cancel the wizard and start it again + setCommitPage(true); +} + +bool ChooseFilePage::isComplete() const +{ + return !pathEdit->text().isEmpty(); +} + +void ChooseFilePage::initializePage() +{ + //HACK: I don't like the 'Commit' button. This hack makes it similar to 'Next' button + setButtonText(QWizard::CommitButton, wizard()->buttonText(QWizard::NextButton)); +} + +bool ChooseFilePage::validatePage() +{ + QString fileName = pathEdit->text(); + + //TODO: allow importing from another session database (*.ttt or *.db) + + QFileInfo f(fileName); + if(f.exists() && f.isFile()) + { + RSImportWizard *w = static_cast(wizard()); + w->startLoadTask(fileName); + return true; + } + + QMessageBox::warning(this, + RsImportStrings::tr("File doesn't exist"), + RsImportStrings::tr("Could not find file '%1'") + .arg(fileName)); + return false; +} + +void ChooseFilePage::onChoose() +{ + RSImportWizard *w = static_cast(wizard()); + + QString title; + QStringList filters; + switch (w->getImportSource()) + { + case RSImportWizard::ImportSource::OdsImport: + { + title = RsImportStrings::tr("Open Spreadsheet"); + filters << FileFormats::tr(FileFormats::odsFormat); + break; + } + case RSImportWizard::ImportSource::SQLiteImport: + { + title = RsImportStrings::tr("Open Session"); + filters << FileFormats::tr(FileFormats::tttFormat); + filters << FileFormats::tr(FileFormats::sqliteFormat); //TODO: forse nascondere il fatto che usiamo un database sqlite + break; + } + default: + break; + } + filters << FileFormats::tr(FileFormats::allFiles); + + QFileDialog dlg(this, title, pathEdit->text()); + dlg.setFileMode(QFileDialog::ExistingFile); + dlg.setAcceptMode(QFileDialog::AcceptOpen); + dlg.setNameFilters(filters); + + if(dlg.exec() != QDialog::Accepted) + return; + + QString fileName = dlg.selectedUrls().value(0).toLocalFile(); + pathEdit->setText(QDir::toNativeSeparators(fileName)); +} diff --git a/src/rollingstock/importer/pages/choosefilepage.h b/src/rollingstock/importer/pages/choosefilepage.h new file mode 100644 index 0000000..126612f --- /dev/null +++ b/src/rollingstock/importer/pages/choosefilepage.h @@ -0,0 +1,27 @@ +#ifndef CHOOSEFILEPAGE_H +#define CHOOSEFILEPAGE_H + +#include + +class QLineEdit; +class QPushButton; + +class ChooseFilePage : public QWizardPage +{ + Q_OBJECT +public: + explicit ChooseFilePage(QWidget *parent = nullptr); + + bool isComplete() const override; + bool validatePage() override; + void initializePage() override; + +private slots: + void onChoose(); + +private: + QLineEdit *pathEdit; + QPushButton *chooseBut; +}; + +#endif // CHOOSEFILEPAGE_H diff --git a/src/rollingstock/importer/pages/fixduplicatesdlg.cpp b/src/rollingstock/importer/pages/fixduplicatesdlg.cpp new file mode 100644 index 0000000..8a7f480 --- /dev/null +++ b/src/rollingstock/importer/pages/fixduplicatesdlg.cpp @@ -0,0 +1,219 @@ +#include "fixduplicatesdlg.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "../intefaces/iduplicatesitemmodel.h" + +#include "utils/model_mode.h" +#include "../rsimportstrings.h" + +#include + +class ProgressDialog : public QProgressDialog +{ +public: + ProgressDialog(FixDuplicatesDlg *parent) : + QProgressDialog(parent), + dlg(parent) + { + setAutoReset(false); + //Manually handle cancel + disconnect(this, SIGNAL(canceled()), this, SLOT(cancel())); + connect(this, SIGNAL(canceled()), this, SLOT(reject())); + } + + void done(int res) override + { + if(res != QDialog::Accepted) + { + res = dlg->warnCancel(this); + if(res == QDialog::Accepted) + return; //Give user a second chance + } + QDialog::done(res); + } + +private: + FixDuplicatesDlg *dlg; +}; + +FixDuplicatesDlg::FixDuplicatesDlg(IDuplicatesItemModel *m, bool enableGoBack, QWidget *parent) : + QDialog(parent), + model(m), + canGoBack(enableGoBack) +{ + QVBoxLayout *lay = new QVBoxLayout(this); + + QLabel *label = new QLabel(RsImportStrings::tr("The file constains some duplicates in item names wich need to be fixed in order to procced.\n" + "There also may be some items with empty name.\n" + "Please assign a custom name to them so that there are no duplicates")); + lay->addWidget(label); + + view = new QTableView; + //Prevent changing names by accidentally pressing a key + view->setEditTriggers(QTableView::DoubleClicked); + view->setModel(model); + view->resizeColumnsToContents(); + lay->addWidget(view); + + + box = new QDialogButtonBox(QDialogButtonBox::Ok); + connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept); + lay->addWidget(box); + + progressDlg = new ProgressDialog(this); + + connect(model, &IDuplicatesItemModel::progressChanged, this, &FixDuplicatesDlg::handleProgress); + connect(model, &IDuplicatesItemModel::progressFinished, progressDlg, &QProgressDialog::accept); + connect(model, &IDuplicatesItemModel::stateChanged, this, &FixDuplicatesDlg::handleModelState); + connect(model, &IDuplicatesItemModel::error, this, &FixDuplicatesDlg::showModelError); + connect(progressDlg, &ProgressDialog::rejected, model, &IDuplicatesItemModel::cancelLoading); + + setWindowTitle(RsImportStrings::tr("Fix Item Names")); + setMinimumSize(450, 200); +} + +void FixDuplicatesDlg::setItemDelegateForColumn(int column, QAbstractItemDelegate *delegate) +{ + view->setItemDelegateForColumn(column, delegate); +} + +void FixDuplicatesDlg::showModelError(const QString& text) +{ + QMessageBox::warning(this, RsImportStrings::tr("Invalid Operation"), text); +} + +void FixDuplicatesDlg::done(int res) +{ + if(res == QDialog::Accepted) + { + //Check if all are fixed + res = blockingReloadCount(IDuplicatesItemModel::LoadingData); + if(res != QDialog::Accepted) + { + QDialog::done(res); + return; + } + + int count = model->getItemCount(); + if(count) + { + QMessageBox msgBox(this); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setWindowTitle(RsImportStrings::tr("Not yet!")); + msgBox.setText(RsImportStrings::tr("There are still %1 items to be fixed").arg(count)); + QPushButton *okBut = msgBox.addButton(QMessageBox::Ok); + QPushButton *backToPrevPage = nullptr; + if(canGoBack) + backToPrevPage = msgBox.addButton(RsImportStrings::tr("Previuos page"), QMessageBox::NoRole); + msgBox.setDefaultButton(okBut); + msgBox.setEscapeButton(okBut); //If dialog gets closed of Esc is pressed act ad if Ok was pressed + msgBox.exec(); + + QAbstractButton *but = msgBox.clickedButton(); + if(but == backToPrevPage && backToPrevPage) + { + res = GoBackToPrevPage; + } + else + { + return; //Give user a second chance + } + } + } + else + { + res = warnCancel(this); + if(res == QDialog::Accepted) + return; //Give user a second chance + } + + return QDialog::done(res); +} + +void FixDuplicatesDlg::handleProgress(int progress, int max) +{ + progressDlg->setMaximum(max); + progressDlg->setValue(progress); +} + +void FixDuplicatesDlg::handleModelState(int state) +{ + QString label; + switch (state) + { + case IDuplicatesItemModel::Loaded: + label = tr("Loaded."); + break; + case IDuplicatesItemModel::Starting: + label = tr("Starting..."); + break; + case IDuplicatesItemModel::CountingItems: + label = tr("Counting items..."); + break; + case IDuplicatesItemModel::LoadingData: + label = tr("Loading data..."); + break; + default: + label = tr("Unknown state."); + } + progressDlg->setLabelText(label); +} + +int FixDuplicatesDlg::blockingReloadCount(int mode) +{ + if(!model->startLoading(mode)) + return canGoBack ? int(GoBackToPrevPage) : int(QDialog::Rejected); + + progressDlg->reset(); + progressDlg->setResult(0); + progressDlg->setModal(true); + + QEventLoop loop; + connect(progressDlg, &QDialog::finished, &loop, &QEventLoop::exit); + int ret = loop.exec(QEventLoop::DialogExec); + progressDlg->reset(); + progressDlg->hide(); + return ret; +} + +int FixDuplicatesDlg::warnCancel(QWidget *w) +{ + //Warn user + QMessageBox msgBox(w); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setWindowTitle(RsImportStrings::tr("Aborting RS Import")); + msgBox.setText(RsImportStrings::tr("If you don't fix duplicated items you cannot proceed.\n" + "Do you wish to Abort the process?")); + QPushButton *abortBut = msgBox.addButton(QMessageBox::Abort); + QPushButton *noBut = msgBox.addButton(QMessageBox::No); + QPushButton *backToPrevPage = nullptr; + if(canGoBack) + backToPrevPage = msgBox.addButton(RsImportStrings::tr("Previuos page"), QMessageBox::NoRole); + msgBox.setDefaultButton(noBut); + msgBox.setEscapeButton(noBut); + + connect(model, &IDuplicatesItemModel::progressFinished, &msgBox, &QMessageBox::accept); + connect(model, &IDuplicatesItemModel::processAborted, &msgBox, &QMessageBox::accept); + msgBox.exec(); + + QAbstractButton *but = msgBox.clickedButton(); + if(but == abortBut) + { + return QDialog::Rejected; + } + else if(but == backToPrevPage && backToPrevPage) + { + return FixDuplicatesDlg::GoBackToPrevPage; + } + + //Give user a second chance + return QDialog::Accepted; +} diff --git a/src/rollingstock/importer/pages/fixduplicatesdlg.h b/src/rollingstock/importer/pages/fixduplicatesdlg.h new file mode 100644 index 0000000..a9279e7 --- /dev/null +++ b/src/rollingstock/importer/pages/fixduplicatesdlg.h @@ -0,0 +1,45 @@ +#ifndef FIXDUPLICATESDLG_H +#define FIXDUPLICATESDLG_H + +#include + +class IDuplicatesItemModel; +class QDialogButtonBox; +class QAbstractItemDelegate; +class QTableView; +class QProgressDialog; + +class FixDuplicatesDlg : public QDialog +{ + Q_OBJECT +public: + + enum { GoBackToPrevPage = QDialog::Accepted + 1 }; + + FixDuplicatesDlg(IDuplicatesItemModel *m, bool enableGoBack, QWidget *parent = nullptr); + + void setItemDelegateForColumn(int column, QAbstractItemDelegate *delegate); + + int blockingReloadCount(int mode); + +public slots: + void done(int res) override; + +private slots: + void showModelError(const QString &text); + void handleProgress(int progress, int max); + void handleModelState(int state); + +private: + int warnCancel(QWidget *w); + +private: + friend class ProgressDialog; + IDuplicatesItemModel *model; + QTableView *view; + QDialogButtonBox *box; + QProgressDialog *progressDlg; + bool canGoBack; +}; + +#endif // FIXDUPLICATESDLG_H diff --git a/src/rollingstock/importer/pages/itemselectionpage.cpp b/src/rollingstock/importer/pages/itemselectionpage.cpp new file mode 100644 index 0000000..585e42f --- /dev/null +++ b/src/rollingstock/importer/pages/itemselectionpage.cpp @@ -0,0 +1,205 @@ +#include "itemselectionpage.h" + +#include +#include +#include +#include +#include +#include + +#include "../rsimportwizard.h" +#include "../intefaces/irsimportmodel.h" +#include "../intefaces/iduplicatesitemmodel.h" +#include "../model/duplicatesimportedrsmodel.h" //HACK + +#include "fixduplicatesdlg.h" +#include "utils/sqldelegate/sqlfkfielddelegate.h" +#include "utils/sqldelegate/modelpageswitcher.h" +#include "rollingstock/rsmatchmodelfactory.h" + +#include "../rsimportstrings.h" +#include "utils/rs_types_names.h" + +static const char *title_strings[] = { + QT_TRANSLATE_NOOP("RsTypeNames", "Owners"), + QT_TRANSLATE_NOOP("RsTypeNames", "Models"), + QT_TRANSLATE_NOOP("RsTypeNames", "Rollingstock") +}; + +static const char *descr_strings[] = { + QT_TRANSLATE_NOOP("RsImportStrings", "Select owners of rollingstock you want to import"), + QT_TRANSLATE_NOOP("RsImportStrings", "Select models of rollingstock you want to import"), + QT_TRANSLATE_NOOP("RsImportStrings", "Select rollingstock pieces you want to import") +}; + +static const char *error_strings[] = { + QT_TRANSLATE_NOOP("RsImportStrings", "You must select at least 1 owner"), + QT_TRANSLATE_NOOP("RsImportStrings", "You must select at least 1 model"), + QT_TRANSLATE_NOOP("RsImportStrings", "You must select at least 1 rollingsock piece") +}; + +static const char *display_strings[] = { //FIXME plurals + QT_TRANSLATE_NOOP("RsImportStrings", "%1 owners selected"), + QT_TRANSLATE_NOOP("RsImportStrings", "%1 models selected"), + QT_TRANSLATE_NOOP("RsImportStrings", "%1 rollingstock pieces selected") +}; + +ItemSelectionPage::ItemSelectionPage(RSImportWizard *w, IRsImportModel *m, QItemEditorFactory *edFactory, IFKField *ifaceDelegate, int delegateCol, ModelModes::Mode mode, QWidget *parent) : + QWizardPage(parent), + mWizard(w), + model(m), + editorFactory(edFactory), + m_mode(mode) +{ + QVBoxLayout *lay = new QVBoxLayout(this); + + //TODO: add SelectAll, SelectNone + + view = new QTableView(this); + view->setEditTriggers(QTableView::DoubleClicked); //Prevent changing names by accidentally pressing a key + lay->addWidget(view); + + auto ps = new ModelPageSwitcher(false, this); + lay->addWidget(ps); + + statusLabel = new QLabel; + lay->addWidget(statusLabel); + + view->setModel(model); + ps->setModel(model); + connect(model, &IRsImportModel::modelError, this, &ItemSelectionPage::showModelError); + + if(ifaceDelegate) + { + SqlFKFieldDelegate *delegate = new SqlFKFieldDelegate(new RSMatchModelFactory(m_mode, model->getDb(), this), ifaceDelegate, this); + view->setItemDelegateForColumn(delegateCol, delegate); + + } + else if(editorFactory) + { + QStyledItemDelegate *delegate = new QStyledItemDelegate(this); + delegate->setItemEditorFactory(editorFactory); + view->setItemDelegateForColumn(delegateCol, delegate); + } + + //Custom colun sorting + //NOTE: leave disconnect() in the old SIGLAL()/SLOT() version in order to work + QHeaderView *header = view->horizontalHeader(); + disconnect(header, SIGNAL(sectionPressed(int)), view, SLOT(selectColumn(int))); + disconnect(header, SIGNAL(sectionEntered(int)), view, SLOT(_q_selectColumn(int))); + + connect(header, &QHeaderView::sectionClicked, this, &ItemSelectionPage::sectionClicked); + header->setSortIndicatorShown(true); + header->setSortIndicator(model->getSortingColumn(), Qt::AscendingOrder); + + connect(model, &IRsImportModel::importCountChanged, this, &ItemSelectionPage::updateStatus); + updateStatus(); + + setTitle(RsTypeNames::tr(title_strings[m_mode])); + setSubTitle(RsImportStrings::tr(descr_strings[m_mode])); + + if(m_mode == ModelModes::Rollingstock) + { + setCommitPage(true); + setButtonText(QWizard::CommitButton, RsImportStrings::tr("Import")); + } +} + +void ItemSelectionPage::initializePage() +{ + //Check for duplicates + std::unique_ptr dupModel; + dupModel.reset(IDuplicatesItemModel::createModel(m_mode, model->getDb(), model, this)); + + bool canGoBack = mWizard->currentId() != RSImportWizard::SelectOwnersIdx; + FixDuplicatesDlg dlg(dupModel.get(), canGoBack, this); + if(m_mode == ModelModes::Rollingstock && editorFactory) + { + QStyledItemDelegate *delegate = new QStyledItemDelegate(this); + delegate->setItemEditorFactory(editorFactory); + dlg.setItemDelegateForColumn(DuplicatesImportedRSModel::NewNumber, delegate); + } + + //First load + int res = dlg.blockingReloadCount(IDuplicatesItemModel::LoadingData); + if(res == FixDuplicatesDlg::GoBackToPrevPage && canGoBack) + { + /* HACK: + * we are inside 'initializePage()' which is called before QWizard + * stores that we are current page + * So if we call 'back()' now QWizard would try to go back by 2 pages + * (that is 1 before current that is already the old page) + * Solution: call it after 'initializePage()' has finished by posting an event + */ + mWizard->goToPrevPageQueued(); + return; + } + else if(res != QDialog::Accepted) + { + //Prevent showing another message box asking user if he is sure about quitting + mWizard->done(RSImportWizard::RejectWithoutAsking); + return; + } + + if(dupModel->getItemCount() > 0) + { + //We have duplicates, run dialog + res = dlg.exec(); + if(res == FixDuplicatesDlg::GoBackToPrevPage && canGoBack) + { + /* HACK: see above */ + mWizard->goToPrevPageQueued(); + return; + } + else if(res != QDialog::Accepted) + { + //Prevent showing another message box asking user if he is sure about quitting + mWizard->done(RSImportWizard::RejectWithoutAsking); + return; + } + } + + //Duplicates are now fixed, refresh main model + model->refreshData(); +} + +void ItemSelectionPage::cleanupPage() +{ + model->clearCache(); +} + +bool ItemSelectionPage::validatePage() +{ + int count = model->countImported(); + if(count == 0) + { + QMessageBox::warning(this, RsImportStrings::tr("Invalid Operation"), RsImportStrings::tr(error_strings[m_mode])); + return false; + } + + model->clearCache(); + + if(m_mode == ModelModes::Rollingstock) + { + mWizard->startImportTask(); + } + + return true; +} + +void ItemSelectionPage::updateStatus() +{ + int count = model->countImported(); + statusLabel->setText(RsImportStrings::tr(display_strings[m_mode]).arg(count)); +} + +void ItemSelectionPage::sectionClicked(int col) +{ + model->setSortingColumn(col); + view->horizontalHeader()->setSortIndicator(model->getSortingColumn(), Qt::AscendingOrder); +} + +void ItemSelectionPage::showModelError(const QString& text) +{ + QMessageBox::warning(this, RsImportStrings::tr("Invalid Operation"), text); +} diff --git a/src/rollingstock/importer/pages/itemselectionpage.h b/src/rollingstock/importer/pages/itemselectionpage.h new file mode 100644 index 0000000..932e253 --- /dev/null +++ b/src/rollingstock/importer/pages/itemselectionpage.h @@ -0,0 +1,42 @@ +#ifndef ITEMSELECTIONPAGE_H +#define ITEMSELECTIONPAGE_H + +#include + +#include "utils/model_mode.h" + +class RSImportWizard; +class QTableView; +class QLabel; + +class IRsImportModel; +class IFKField; +class QItemEditorFactory; + +class ItemSelectionPage : public QWizardPage +{ + Q_OBJECT +public: + ItemSelectionPage(RSImportWizard *w, IRsImportModel *m, QItemEditorFactory *edFactory, IFKField *ifaceDelegate, int delegateCol, ModelModes::Mode mode, QWidget *parent = nullptr); + + virtual void initializePage() override; + virtual void cleanupPage() override; + virtual bool validatePage() override; + +private slots: + void updateStatus(); + void sectionClicked(int col); + void showModelError(const QString &text); + +private: + RSImportWizard *mWizard; + IRsImportModel *model; + QItemEditorFactory *editorFactory; + + QTableView *view; + QLabel *statusLabel; + + ModelModes::Mode m_mode; +}; + +#endif // ITEMSELECTIONPAGE_H diff --git a/src/rollingstock/importer/pages/loadingpage.cpp b/src/rollingstock/importer/pages/loadingpage.cpp new file mode 100644 index 0000000..cb4ee1f --- /dev/null +++ b/src/rollingstock/importer/pages/loadingpage.cpp @@ -0,0 +1,35 @@ +#include "loadingpage.h" +#include "../rsimportwizard.h" +#include "../backends/loadprogressevent.h" + +#include +#include + +LoadingPage::LoadingPage(RSImportWizard *w, QWidget *parent) : + QWizardPage(parent), + mWizard(w) +{ + QVBoxLayout *lay = new QVBoxLayout(this); + + progressBar = new QProgressBar; + lay->addWidget(progressBar); +} + +bool LoadingPage::isComplete() const +{ + return !mWizard->taskRunning(); +} + +void LoadingPage::handleProgress(int pr, int max) +{ + if(max == LoadProgressEvent::ProgressMaxFinished) + { + progressBar->setValue(progressBar->maximum()); + emit completeChanged(); + } + else + { + progressBar->setMaximum(max); + progressBar->setValue(pr); + } +} diff --git a/src/rollingstock/importer/pages/loadingpage.h b/src/rollingstock/importer/pages/loadingpage.h new file mode 100644 index 0000000..f96d8cf --- /dev/null +++ b/src/rollingstock/importer/pages/loadingpage.h @@ -0,0 +1,24 @@ +#ifndef LOADFILEPAGE_H +#define LOADFILEPAGE_H + +#include + +class RSImportWizard; +class QProgressBar; + +class LoadingPage : public QWizardPage +{ + Q_OBJECT +public: + LoadingPage(RSImportWizard *w, QWidget *parent = nullptr); + + virtual bool isComplete() const override; + + void handleProgress(int pr, int max); + +private: + RSImportWizard *mWizard; + QProgressBar *progressBar; +}; + +#endif // LOADFILEPAGE_H diff --git a/src/rollingstock/importer/pages/optionspage.cpp b/src/rollingstock/importer/pages/optionspage.cpp new file mode 100644 index 0000000..ec28524 --- /dev/null +++ b/src/rollingstock/importer/pages/optionspage.cpp @@ -0,0 +1,190 @@ +#include "optionspage.h" +#include "../rsimportwizard.h" + +#include "../backends/ioptionswidget.h" +#include "../backends/importmodes.h" + +#include "../rsimportstrings.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include "utils/rs_types_names.h" + +OptionsPage::OptionsPage(QWidget *parent) : + QWizardPage(parent), + optionsWidget(nullptr) +{ + QVBoxLayout *lay = new QVBoxLayout(this); + + //General options + generalBox = new QGroupBox(RsImportStrings::tr("General options")); + QFormLayout *generalLay = new QFormLayout(generalBox); + + importOwners = new QCheckBox(RsImportStrings::tr("Import rollingstick owners")); + connect(importOwners, &QCheckBox::toggled, this, &OptionsPage::updateGeneralCheckBox); + generalLay->addRow(importOwners); + + importModels = new QCheckBox(RsImportStrings::tr("Import rollingstick models")); + connect(importModels, &QCheckBox::toggled, this, &OptionsPage::updateGeneralCheckBox); + generalLay->addRow(importModels); + + importRS = new QCheckBox(RsImportStrings::tr("Import rollingstick pieces")); + connect(importRS, &QCheckBox::toggled, this, &OptionsPage::updateGeneralCheckBox); + generalLay->addRow(importRS); + + //NOTE: see 'RollingStockManager::setupPages()' in 'Setup delegates' section + defaultSpeedSpin = new QSpinBox; + defaultSpeedSpin->setRange(1, 999); + defaultSpeedSpin->setSuffix(" km/h"); + defaultSpeedSpin->setValue(120); + defaultSpeedSpin->setToolTip(tr("Default speed is applied when a rollingstock model is not matched to an existing one" + " and has to be created from scratch")); + generalLay->addRow(tr("Default speed"), defaultSpeedSpin); + + defaultTypeCombo = new QComboBox; + QStringList list; + list.reserve(int(RsType::NTypes)); + for(int i = 0; i < int(RsType::NTypes); i++) + list.append(RsTypeNames::name(RsType(i))); + QStringListModel *rsTypeModel = new QStringListModel(list, this); + defaultTypeCombo->setModel(rsTypeModel); + defaultTypeCombo->setCurrentIndex(int(RsType::FreightWagon)); + defaultTypeCombo->setToolTip(tr("Default type is applied when a rollingstock model is not matched to an existing one" + " and has to be created from scratch")); + generalLay->addRow(tr("Default type"), defaultTypeCombo); + + lay->addWidget(generalBox); + + //Specific options + specificBox = new QGroupBox(RsImportStrings::tr("Import options")); + QFormLayout *specificlLay = new QFormLayout(specificBox); + sourceCombo = new QComboBox; + connect(sourceCombo, static_cast(&QComboBox::activated), this, &OptionsPage::setSource); + specificlLay->addRow(RsImportStrings::tr("Import source:"), sourceCombo); + scrollArea = new QScrollArea; + scrollArea->setWidgetResizable(true); + scrollArea->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + specificlLay->addRow(scrollArea); + specificBox->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + lay->addWidget(specificBox); +} + +void OptionsPage::initializePage() +{ + const RSImportWizard *w = static_cast(wizard()); + sourceCombo->setModel(w->getSourcesModel()); + setSource(int(w->getImportSource())); + setMode(w->getImportMode()); +} + +bool OptionsPage::validatePage() +{ + RSImportWizard *w = static_cast(wizard()); + RSImportWizard::ImportSource source = RSImportWizard::ImportSource(sourceCombo->currentIndex()); + if(sourceCombo->currentIndex() < 0 || !optionsWidget) + return false; + w->setSource(source, optionsWidget); + w->setImportMode(getMode()); + return true; +} + +void OptionsPage::setMode(int m) +{ + if(m == 0) + m = RSImportMode::ImportRSPieces; + if(m & RSImportMode::ImportRSPieces) + { + m = RSImportMode::ImportRSOwners | RSImportMode::ImportRSModels; + + importRS->blockSignals(true); + importRS->setChecked(true); + importRS->blockSignals(false); + } + if(m & RSImportMode::ImportRSOwners) + { + importOwners->blockSignals(true); + importOwners->setChecked(true); + importOwners->blockSignals(false); + } + if(m & RSImportMode::ImportRSModels) + { + importModels->blockSignals(true); + importModels->setChecked(true); + importModels->blockSignals(false); + } + updateGeneralCheckBox(); +} + +int OptionsPage::getMode() +{ + int mode = 0; + if(importOwners->isChecked()) + mode |= RSImportMode::ImportRSOwners; + if(importModels->isChecked()) + mode |= RSImportMode::ImportRSModels; + if(importRS->isChecked()) + mode |= RSImportMode::ImportRSPieces; + return mode; +} + +void OptionsPage::setSource(int s) +{ + sourceCombo->setCurrentIndex(s); + if(optionsWidget) + { + scrollArea->takeWidget(); + delete optionsWidget; + optionsWidget = nullptr; + } + + RSImportWizard *w = static_cast(wizard()); + optionsWidget = w->createOptionsWidget(RSImportWizard::ImportSource(s), this); + scrollArea->setWidget(optionsWidget); +} + +void OptionsPage::updateGeneralCheckBox() +{ + if(!importOwners->isChecked() && !importModels->isChecked()) + { + QMessageBox::warning(this, RsImportStrings::tr("Invalid option"), + RsImportStrings::tr("You must at least import owners or models")); + importModels->setChecked(true); //Tiggers an updateGeneralCheckBox() so return + return; + } + + importRS->blockSignals(true); + + if(importOwners->isChecked() && importModels->isChecked()) + { + importRS->setEnabled(true); + importRS->setToolTip(QString()); + } + else + { + if(importRS->isChecked()) + { + QMessageBox::warning(this, RsImportStrings::tr("No rolloingstock imported"), + RsImportStrings::tr("No rollingstock piece will be imported.\n" + "In order to import rollingstock pieces you must also import models and owners.")); + } + + importRS->setEnabled(false); + importRS->setChecked(false); + importRS->setToolTip(RsImportStrings::tr("In order to import rollingstock pieces you must also import models and owners.")); + } + importRS->blockSignals(false); + + //Default type and speed have meaning only if importing models + defaultSpeedSpin->setEnabled(importModels->isChecked()); + defaultTypeCombo->setEnabled(importModels->isChecked()); +} diff --git a/src/rollingstock/importer/pages/optionspage.h b/src/rollingstock/importer/pages/optionspage.h new file mode 100644 index 0000000..b24d56e --- /dev/null +++ b/src/rollingstock/importer/pages/optionspage.h @@ -0,0 +1,44 @@ +#ifndef OPTIONSPAGE_H +#define OPTIONSPAGE_H + +#include + +class QGroupBox; +class QCheckBox; +class QComboBox; +class QSpinBox; +class IOptionsWidget; +class QScrollArea; + +class OptionsPage : public QWizardPage +{ + Q_OBJECT +public: + explicit OptionsPage(QWidget *parent = nullptr); + + virtual void initializePage() override; + virtual bool validatePage() override; + +private slots: + void updateGeneralCheckBox(); + void setSource(int s); + +private: + void setMode(int m); + int getMode(); + +private: + QGroupBox *generalBox; + QCheckBox *importOwners; + QCheckBox *importModels; + QCheckBox *importRS; + QSpinBox *defaultSpeedSpin; + QComboBox *defaultTypeCombo; + + QGroupBox *specificBox; + QComboBox *sourceCombo; + IOptionsWidget *optionsWidget; + QScrollArea *scrollArea; +}; + +#endif // OPTIONSPAGE_H diff --git a/src/rollingstock/importer/rsimportstrings.h b/src/rollingstock/importer/rsimportstrings.h new file mode 100644 index 0000000..bcf2e72 --- /dev/null +++ b/src/rollingstock/importer/rsimportstrings.h @@ -0,0 +1,11 @@ +#ifndef RAIMPORTSTRINGS_H +#define RAIMPORTSTRINGS_H + +#include + +class RsImportStrings +{ + Q_DECLARE_TR_FUNCTIONS(RsImportStrings) +}; + +#endif // RAIMPORTSTRINGS_H diff --git a/src/rollingstock/importer/rsimportwizard.cpp b/src/rollingstock/importer/rsimportwizard.cpp new file mode 100644 index 0000000..40745fe --- /dev/null +++ b/src/rollingstock/importer/rsimportwizard.cpp @@ -0,0 +1,384 @@ +#include "rsimportwizard.h" +#include "rsimportstrings.h" + +#include "model/rsimportedmodelsmodel.h" +#include "model/rsimportedownersmodel.h" +#include "model/rsimportedrollingstockmodel.h" + +#include "app/session.h" + +#include "pages/optionspage.h" +#include "pages/choosefilepage.h" +#include "pages/loadingpage.h" +#include "pages/itemselectionpage.h" + +#include "utils/spinbox/spinboxeditorfactory.h" +#include + +#include "backends/ioptionswidget.h" +#include "backends/optionsmodel.h" +#include "backends/loadtaskutils.h" +#include "backends/loadprogressevent.h" +#include "backends/importtask.h" +#include + +//Backends +#include "backends/ods/loadodstask.h" +#include "backends/ods/odsoptionswidget.h" +#include "backends/sqlite/loadsqlitetask.h" +#include "backends/sqlite/sqliteoptionswidget.h" + +#include +#include + +RSImportWizard::RSImportWizard(bool resume, QWidget *parent) : + QWizard (parent), + loadTask(nullptr), + importTask(nullptr), + isStoppingTask(false), + defaultSpeed(120), + defaultRsType(RsType::FreightWagon), + importMode(RSImportMode::ImportRSPieces), + importSource(ImportSource::OdsImport) +{ + sourcesModel = new OptionsModel(this); + + modelsModel = new RSImportedModelsModel(Session->m_Db, this); + ownersModel = new RSImportedOwnersModel(Session->m_Db, this); + listModel = new RSImportedRollingstockModel(Session->m_Db, this); + + loadFilePage = new LoadingPage(this); + loadFilePage->setCommitPage(true); + loadFilePage->setTitle(RsImportStrings::tr("File loading")); + loadFilePage->setSubTitle(RsImportStrings::tr("Parsing file data...")); + + //HACK: I don't like the 'Commit' button. This hack makes it similar to 'Next' button + loadFilePage->setButtonText(QWizard::CommitButton, buttonText(QWizard::NextButton)); + + importPage = new LoadingPage(this); + importPage->setTitle(RsImportStrings::tr("Importing")); + importPage->setSubTitle(RsImportStrings::tr("Importing data...")); + + spinFactory = new SpinBoxEditorFactory; + spinFactory->setRange(-1, 99999); + spinFactory->setSpecialValueText(RsImportStrings::tr("Original")); + spinFactory->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + + setPage(OptionsPageIdx, new OptionsPage); + setPage(ChooseFileIdx, new ChooseFilePage); + setPage(LoadFileIdx, loadFilePage); + setPage(SelectOwnersIdx, new ItemSelectionPage(this, ownersModel, nullptr, ownersModel, RSImportedOwnersModel::MatchExisting, ModelModes::Owners)); + setPage(SelectModelsIdx, new ItemSelectionPage(this, modelsModel, nullptr, modelsModel, RSImportedModelsModel::MatchExisting, ModelModes::Models)); + setPage(SelectRsIdx, new ItemSelectionPage(this, listModel, spinFactory, nullptr, RSImportedRollingstockModel::NewNumber, ModelModes::Rollingstock)); + setPage(ImportRsIdx, importPage); + + if(resume) + setStartId(SelectOwnersIdx); + + resize(700, 500); +} + +RSImportWizard::~RSImportWizard() +{ + abortLoadTask(); + abortImportTask(); + delete spinFactory; +} + +void RSImportWizard::done(int result) +{ + if(result == QDialog::Rejected || result == RejectWithoutAsking) + { + if(!isStoppingTask) + { + if(result == QDialog::Rejected) //RejectWithoutAsking skips this + { + QMessageBox msgBox(this); + msgBox.setIcon(QMessageBox::Question); + msgBox.setWindowTitle(RsImportStrings::tr("Abort import?")); + msgBox.setText(RsImportStrings::tr("Do you want to import process? No data will be imported")); + QPushButton *abortBut = msgBox.addButton(QMessageBox::Abort); + QPushButton *noBut = msgBox.addButton(QMessageBox::No); + msgBox.setDefaultButton(noBut); + msgBox.setEscapeButton(noBut); //Do not Abort if dialog is closed by Esc or X window button + msgBox.exec(); + if(msgBox.clickedButton() != abortBut) + return; + } + + if(loadTask) + { + loadTask->stop(); + isStoppingTask = true; + loadFilePage->setSubTitle(RsImportStrings::tr("Aborting...")); + } + + if(importTask) + { + importTask->stop(); + isStoppingTask = true; + importPage->setSubTitle(RsImportStrings::tr("Aborting...")); + } + } + else + { + if(loadTask || importTask) + return; //Already sent 'stop', just wait + } + + //Reset to standard value because QWizard doesn't know about RejectWithoutAsking + result = QDialog::Rejected; + } + + //Clear tables after import process completed or was aborted + Session->clearImportRSTables(); + + QWizard::done(result); +} + +void RSImportWizard::initializePage(int id) +{ + QWizard::initializePage(id); +} + +void RSImportWizard::cleanupPage(int id) +{ + QWizard::cleanupPage(id); +} + +bool RSImportWizard::validateCurrentPage() +{ + if(QWizard::validateCurrentPage()) + { + if(nextId() == ImportRsIdx) + { + startImportTask(); + } + return true; + } + return false; +} + +int RSImportWizard::nextId() const +{ + int id = QWizard::nextId(); + switch (currentId()) + { + case LoadFileIdx: + { + if((importMode & RSImportMode::ImportRSOwners) == 0) + { + //Skip owners page + id = SelectModelsIdx; + } + break; + } + case SelectOwnersIdx: + { + if((importMode & RSImportMode::ImportRSModels) == 0) + { + //Skip models and rollingstock pages + id = ImportRsIdx; + } + break; + } + case SelectModelsIdx: + { + if((importMode & RSImportMode::ImportRSPieces) == 0) + { + //Skip rollingstock page + id = ImportRsIdx; + } + break; + } + } + return id; +} + +bool RSImportWizard::event(QEvent *e) +{ + if(e->type() == LoadProgressEvent::_Type) + { + LoadProgressEvent *ev = static_cast(e); + ev->setAccepted(true); + + if(ev->task == loadTask) + { + QString errText; + if(ev->max == LoadProgressEvent::ProgressMaxFinished) + { + if(ev->progress == LoadProgressEvent::ProgressError) + { + errText = loadTask->getErrorText(); + } + + loadFilePage->setSubTitle(tr("Completed.")); + + //Delete task before handling event because otherwise it is detected as still running + delete loadTask; + loadTask = nullptr; + } + + loadFilePage->handleProgress(ev->progress, ev->max); + + if(ev->progress == LoadProgressEvent::ProgressError) + { + QMessageBox::warning(this, RsImportStrings::tr("Loading Error"), errText); + reject(); + } + else if(ev->progress == LoadProgressEvent::ProgressAbortedByUser) + { + reject(); //Reject the second time + } + } + else if(ev->task == importTask) + { + if(ev->max == LoadProgressEvent::ProgressMaxFinished) + { + //Delete task before handling event because otherwise it is detected as still running + delete importTask; + importTask = nullptr; + } + + importPage->handleProgress(ev->progress, ev->max); + + if(ev->progress == LoadProgressEvent::ProgressError) + { + //QMessageBox::warning(this, RsImportStrings::tr("Loading Error"), errText); TODO + reject(); + } + else if(ev->progress == LoadProgressEvent::ProgressAbortedByUser) + { + reject(); //Reject the second time + } + } + + return true; + } + else if(e->type() == QEvent::Type(CustomEvents::RsImportGoBackPrevPage)) + { + e->setAccepted(true); + back(); + } + return QWizard::event(e); +} + + + +bool RSImportWizard::startLoadTask(const QString& fileName) +{ + abortLoadTask(); + + //Clear tables before starting new import process + Session->clearImportRSTables(); + + loadTask = createLoadTask(optionsMap, fileName); + + if(!loadTask) + { + QMessageBox::warning(this, RsImportStrings::tr("Error"), + RsImportStrings::tr("Invalid option selected. Please try again.")); + return false; + } + + QThreadPool::globalInstance()->start(loadTask); + return true; +} + +void RSImportWizard::abortLoadTask() +{ + if(loadTask) + { + loadTask->cleanup(); + loadTask->stop(); + loadTask = nullptr; + } +} + +void RSImportWizard::startImportTask() +{ + abortImportTask(); + + importTask = new ImportTask(Session->m_Db, this); + QThreadPool::globalInstance()->start(importTask); +} + +void RSImportWizard::abortImportTask() +{ + if(importTask) + { + importTask->cleanup(); + importTask->stop(); + importTask = nullptr; + } +} + +void RSImportWizard::goToPrevPageQueued() +{ + qApp->postEvent(this, new QEvent(QEvent::Type(CustomEvents::RsImportGoBackPrevPage))); +} + +void RSImportWizard::setDefaultTypeAndSpeed(RsType t, int speed) +{ + defaultRsType = t; + defaultSpeed = speed; +} + +void RSImportWizard::setImportMode(int m) +{ + if(m == 0) + m = RSImportMode::ImportRSPieces; + if(m & RSImportMode::ImportRSPieces) + m |= RSImportMode::ImportRSOwners | RSImportMode::ImportRSModels; + importMode = m; +} + +IOptionsWidget *RSImportWizard::createOptionsWidget(RSImportWizard::ImportSource source, QWidget *parent) +{ + IOptionsWidget *w = nullptr; + switch (source) + { + case ImportSource::OdsImport: + w = new ODSOptionsWidget(parent); + break; + case ImportSource::SQLiteImport: + w = new SQLiteOptionsWidget(parent); + break; + default: + break; + } + + if(w) + { + w->loadSettings(optionsMap); + } + return w; +} + +void RSImportWizard::setSource(RSImportWizard::ImportSource source, IOptionsWidget *options) +{ + importSource = source; + optionsMap.clear(); + options->saveSettings(optionsMap); +} + +ILoadRSTask *RSImportWizard::createLoadTask(const QMap &arguments, const QString& fileName) +{ + ILoadRSTask *task = nullptr; + switch (importSource) + { + case ImportSource::OdsImport: + { + task = new LoadODSTask(arguments, Session->m_Db, importMode, defaultSpeed, defaultRsType, fileName, this); + break; + } + case ImportSource::SQLiteImport: + { + task = new LoadSQLiteTask(Session->m_Db, importMode, fileName, this); + break; + } + default: + break; + } + return task; +} diff --git a/src/rollingstock/importer/rsimportwizard.h b/src/rollingstock/importer/rsimportwizard.h new file mode 100644 index 0000000..c711868 --- /dev/null +++ b/src/rollingstock/importer/rsimportwizard.h @@ -0,0 +1,110 @@ +#ifndef RSIMPORTWIZARD_H +#define RSIMPORTWIZARD_H + +#include + +#include +#include + +#include "utils/types.h" + +class RSImportedOwnersModel; +class RSImportedModelsModel; +class RSImportedRollingstockModel; + +class ILoadRSTask; +class ImportTask; +class LoadingPage; +class SpinBoxEditorFactory; +class QAbstractItemModel; +class IOptionsWidget; + +class RSImportWizard : public QWizard +{ + Q_OBJECT +public: + enum class ImportSource + { + OdsImport = 0, + SQLiteImport, + NSources + }; + + enum Pages + { + OptionsPageIdx = 0, + ChooseFileIdx, + LoadFileIdx, + SelectOwnersIdx, + SelectModelsIdx, + SelectRsIdx, + ImportRsIdx, + NPages + }; + + enum ExtraCodes + { + RejectWithoutAsking = 3 + }; + + explicit RSImportWizard(bool resume, QWidget *parent = nullptr); + ~RSImportWizard() override; + + void done(int result) override; + virtual bool validateCurrentPage() override; + virtual int nextId() const override; + + bool startLoadTask(const QString &fileName); + void abortLoadTask(); + inline bool taskRunning() const { return loadTask || importTask; } + + void startImportTask(); + void abortImportTask(); + + void goToPrevPageQueued(); + +public: //Settings + void setDefaultTypeAndSpeed(RsType t, int speed); + + inline int getImportMode() const { return importMode; } + void setImportMode(int m); + + inline ImportSource getImportSource() const { return importSource; } + + inline QAbstractItemModel *getSourcesModel() const + { + return sourcesModel; + } + + IOptionsWidget *createOptionsWidget(ImportSource source, QWidget *parent); + void setSource(ImportSource source, IOptionsWidget *options); + + ILoadRSTask *createLoadTask(const QMap& arguments, const QString &fileName); + +protected: + bool event(QEvent *e) override; + void initializePage(int id) override; + void cleanupPage(int id) override; + +private: + RSImportedOwnersModel *ownersModel; + RSImportedModelsModel *modelsModel; + RSImportedRollingstockModel *listModel; + SpinBoxEditorFactory *spinFactory; + + ILoadRSTask *loadTask; + ImportTask *importTask; + LoadingPage *loadFilePage; + LoadingPage *importPage; + bool isStoppingTask; + + //Import options + int defaultSpeed; + RsType defaultRsType; + int importMode; + ImportSource importSource; + QAbstractItemModel *sourcesModel; + QMap optionsMap; +}; + +#endif // RSIMPORTWIZARD_H diff --git a/src/rollingstock/manager/CMakeLists.txt b/src/rollingstock/manager/CMakeLists.txt new file mode 100644 index 0000000..1980c75 --- /dev/null +++ b/src/rollingstock/manager/CMakeLists.txt @@ -0,0 +1,16 @@ +add_subdirectory(delegates) +add_subdirectory(dialogs) + +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + rollingstock/rollingstockmatchmodel.h + rollingstock/manager/rollingstockmanager.h + + rollingstock/manager/rollingstockmanager.cpp + PARENT_SCOPE +) + +set(TRAINTIMETABLE_UI_FILES + ${TRAINTIMETABLE_UI_FILES} + PARENT_SCOPE +) diff --git a/src/rollingstock/manager/delegates/CMakeLists.txt b/src/rollingstock/manager/delegates/CMakeLists.txt new file mode 100644 index 0000000..6499a48 --- /dev/null +++ b/src/rollingstock/manager/delegates/CMakeLists.txt @@ -0,0 +1,11 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + rollingstock/manager/delegates/rsnumberdelegate.h + rollingstock/manager/delegates/rsnumspinbox.h + rollingstock/manager/delegates/rstypedelegate.h + + rollingstock/manager/delegates/rsnumberdelegate.cpp + rollingstock/manager/delegates/rsnumspinbox.cpp + rollingstock/manager/delegates/rstypedelegate.cpp + PARENT_SCOPE +) diff --git a/src/rollingstock/manager/delegates/rsnumberdelegate.cpp b/src/rollingstock/manager/delegates/rsnumberdelegate.cpp new file mode 100644 index 0000000..87ac7a5 --- /dev/null +++ b/src/rollingstock/manager/delegates/rsnumberdelegate.cpp @@ -0,0 +1,55 @@ +#include "rsnumberdelegate.h" + +#include "utils/model_roles.h" + +#include "rsnumspinbox.h" + +//TODO: remove +RsNumberDelegate::RsNumberDelegate(QObject *parent) : + QStyledItemDelegate(parent) +{ + +} + +QWidget *RsNumberDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &/*option*/, const QModelIndex &index) const +{ + QSpinBox *ed = nullptr; + QVariant v = index.data(RS_IS_ENGINE); + if(!v.toBool()) + { + //Custom spinbox for Wagons 'XXX-X' + ed = new RsNumSpinBox(parent); + } + else + { + //Normal spinbox for Engines + ed = new QSpinBox(parent); + ed->setRange(0, 9999); + } + + return ed; +} + +void RsNumberDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QSpinBox *ed = static_cast(editor); + QVariant v = index.data(RS_NUMBER); + int val = 0; + if(v.isValid()) + { + val = v.toInt(); + } + + ed->setValue(val); +} + +void RsNumberDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + QSpinBox *ed = static_cast(editor); + model->setData(index, ed->value(), RS_NUMBER); +} + +void RsNumberDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &/*index*/) const +{ + editor->setGeometry(option.rect); +} diff --git a/src/rollingstock/manager/delegates/rsnumberdelegate.h b/src/rollingstock/manager/delegates/rsnumberdelegate.h new file mode 100644 index 0000000..433ee7f --- /dev/null +++ b/src/rollingstock/manager/delegates/rsnumberdelegate.h @@ -0,0 +1,27 @@ +#ifndef RSNUMBERDELEGATE_H +#define RSNUMBERDELEGATE_H + +#include + +class RsNumberDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit RsNumberDelegate(QObject *parent = nullptr); + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override; + + void updateEditorGeometry(QWidget *editor, + const QStyleOptionViewItem &option, const QModelIndex &index) const override; + +signals: + +public slots: +}; + +#endif // RSNUMBERDELEGATE_H diff --git a/src/rollingstock/manager/delegates/rsnumspinbox.cpp b/src/rollingstock/manager/delegates/rsnumspinbox.cpp new file mode 100644 index 0000000..76c508d --- /dev/null +++ b/src/rollingstock/manager/delegates/rsnumspinbox.cpp @@ -0,0 +1,38 @@ +#include "rsnumspinbox.h" + +//TODO: remove +RsNumSpinBox::RsNumSpinBox(QWidget *parent) : + QSpinBox(parent) +{ + setRange(0, 9999); //from 000-0 to 999-9 +} + +QValidator::State RsNumSpinBox::validate(QString &input, int &/*pos*/) const +{ + QString s = input; + s.remove(QChar('-')); + bool ok = false; + int val = s.toInt(&ok); + if(ok && val >= minimum() && val <= maximum()) + return QValidator::Acceptable; + return QValidator::Invalid; +} + +int RsNumSpinBox::valueFromText(const QString &str) const +{ + QString s = str; + s.remove(QChar('-')); + bool ok = false; + int val = s.toInt(&ok); + if(ok) + return val; + return 0; +} + +QString RsNumSpinBox::textFromValue(int val) const +{ + QString str = QString::number(val); + str = str.rightJustified(4, QChar('0')); + str.insert(3, QChar('-')); + return str; //XXX-X +} diff --git a/src/rollingstock/manager/delegates/rsnumspinbox.h b/src/rollingstock/manager/delegates/rsnumspinbox.h new file mode 100644 index 0000000..43435eb --- /dev/null +++ b/src/rollingstock/manager/delegates/rsnumspinbox.h @@ -0,0 +1,19 @@ +#ifndef RSNUMSPINBOX_H +#define RSNUMSPINBOX_H + +#include + +class RsNumSpinBox : public QSpinBox +{ + Q_OBJECT +public: + explicit RsNumSpinBox(QWidget *parent = nullptr); + + virtual QValidator::State validate(QString &input, int &pos) const override; + +protected: + int valueFromText(const QString& text) const override; + QString textFromValue(int val) const override; +}; + +#endif // RSNUMSPINBOX_H diff --git a/src/rollingstock/manager/delegates/rstypedelegate.cpp b/src/rollingstock/manager/delegates/rstypedelegate.cpp new file mode 100644 index 0000000..2827a55 --- /dev/null +++ b/src/rollingstock/manager/delegates/rstypedelegate.cpp @@ -0,0 +1,62 @@ +#include "rstypedelegate.h" + +#include "utils/types.h" +#include "utils/rs_types_names.h" + +#include "utils/model_roles.h" + +#include + +RSTypeDelegate::RSTypeDelegate(bool subType, QObject *parent) : + QStyledItemDelegate (parent), + m_subType(subType) +{ + QStringList list; + if(subType) + { + list.reserve(int(RsEngineSubType::NTypes)); + for(int i = 0; i < int(RsEngineSubType::NTypes); i++) + list.append(RsTypeNames::name(RsEngineSubType(i))); + } + else + { + list.reserve(int(RsType::NTypes)); + for(int i = 0; i < int(RsType::NTypes); i++) + list.append(RsTypeNames::name(RsType(i))); + } + + comboModel.setStringList(list); +} + +QWidget *RSTypeDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &/*options*/, const QModelIndex &/*idx*/) const +{ + QComboBox *combo = new QComboBox(parent); + combo->setModel(const_cast(&comboModel)); + return combo; +} + +void RSTypeDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QComboBox *combo = static_cast(editor); + int type = index.data(m_subType ? RS_SUB_TYPE_ROLE : RS_TYPE_ROLE).toInt(); + connect(combo, static_cast(&QComboBox::activated), this, &RSTypeDelegate::onItemClicked); + combo->setCurrentIndex(type); + combo->showPopup(); +} + +void RSTypeDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + QComboBox *combo = static_cast(editor); + int type = combo->currentIndex(); + model->setData(index, type, m_subType ? RS_SUB_TYPE_ROLE : RS_TYPE_ROLE); +} + +void RSTypeDelegate::onItemClicked() +{ + QComboBox *combo = qobject_cast(sender()); + if(combo) + { + commitData(combo); + closeEditor(combo); + } +} diff --git a/src/rollingstock/manager/delegates/rstypedelegate.h b/src/rollingstock/manager/delegates/rstypedelegate.h new file mode 100644 index 0000000..0afeb26 --- /dev/null +++ b/src/rollingstock/manager/delegates/rstypedelegate.h @@ -0,0 +1,26 @@ +#ifndef RSTYPEDELEGATE_H +#define RSTYPEDELEGATE_H + +#include +#include + +class RSTypeDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit RSTypeDelegate(bool subType, QObject *parent = nullptr); + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &options, const QModelIndex &idx) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override; + +private slots: + void onItemClicked(); + +private: + QStringListModel comboModel; + bool m_subType; +}; + +#endif // RSTYPEDELEGATE_H diff --git a/src/rollingstock/manager/dialogs/CMakeLists.txt b/src/rollingstock/manager/dialogs/CMakeLists.txt new file mode 100644 index 0000000..879317b --- /dev/null +++ b/src/rollingstock/manager/dialogs/CMakeLists.txt @@ -0,0 +1,16 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + rollingstock/manager/dialogs/mergemodelsdialog.h + rollingstock/manager/dialogs/mergeownersdialog.h + + rollingstock/manager/dialogs/mergemodelsdialog.cpp + rollingstock/manager/dialogs/mergeownersdialog.cpp + PARENT_SCOPE +) + +set(TRAINTIMETABLE_UI_FILES + ${TRAINTIMETABLE_UI_FILES} + rollingstock/manager/dialogs/mergemodelsdialog.ui + rollingstock/manager/dialogs/mergeownersdialog.ui + PARENT_SCOPE +) diff --git a/src/rollingstock/manager/dialogs/mergemodelsdialog.cpp b/src/rollingstock/manager/dialogs/mergemodelsdialog.cpp new file mode 100644 index 0000000..dacf8d4 --- /dev/null +++ b/src/rollingstock/manager/dialogs/mergemodelsdialog.cpp @@ -0,0 +1,189 @@ +#include "mergemodelsdialog.h" +#include "ui_mergemodelsdialog.h" + +#include "app/session.h" + +#include "rollingstock/rsmodelsmatchmodel.h" + +#include "utils/rs_types_names.h" + +#include +#include "utils/sqldelegate/customcompletionlineedit.h" + +#include + +MergeModelsDialog::MergeModelsDialog(sqlite3pp::database &db, QWidget *parent) : + QDialog(parent), + ui(new Ui::MergeModelsDialog), + mDb(db), + q_getModelInfo(mDb, "SELECT suffix,max_speed,axes,type,sub_type FROM rs_models WHERE id=?") +{ + ui->setupUi(this); + + model = new RSModelsMatchModel(mDb, this); + sourceModelEdit = new CustomCompletionLineEdit(model); + destModelEdit = new CustomCompletionLineEdit(model); + + sourceModelEdit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + destModelEdit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + + ui->gridLayout->addWidget(sourceModelEdit, 1, 0); + ui->gridLayout->addWidget(destModelEdit, 2, 0); + + connect(sourceModelEdit, &CustomCompletionLineEdit::dataIdChanged, this, &MergeModelsDialog::sourceModelChanged); + connect(destModelEdit, &CustomCompletionLineEdit::dataIdChanged, this, &MergeModelsDialog::destModelChanged); + + ui->removeSourceCheckBox->setChecked(AppSettings.getRemoveMergedSourceModel()); +} + +MergeModelsDialog::~MergeModelsDialog() +{ + delete ui; +} + +void MergeModelsDialog::done(int r) +{ + if(r == QDialog::Accepted) + { + db_id sourceModelId = 0; + db_id destModelId = 0; + QString tmp; + sourceModelEdit->getData(sourceModelId, tmp); + destModelEdit->getData(destModelId, tmp); + + //Check input is valid + if(!sourceModelId || !destModelId || sourceModelId == destModelId) + { + QMessageBox::warning(this, tr("Invalid Models"), tr("Models must not be null and must be different")); + return; //We don't want the dialog to be closed + } + + if(mergeModels(sourceModelId, destModelId, ui->removeSourceCheckBox->isChecked())) + { + //Operation succeded, inform user + QMessageBox::information(this, + tr("Merging completed"), + tr("Models merged succesfully.")); + } + else + { + QMessageBox::warning(this, tr("Error while merging"), tr("Some error occurred while merging models.")); + //Accept dialog to close it, so don't return here + } + } + + QDialog::done(r); +} + +void MergeModelsDialog::sourceModelChanged(db_id modelId) +{ + fillModelInfo(ui->sourceModelInfo, modelId); +} + +void MergeModelsDialog::destModelChanged(db_id modelId) +{ + fillModelInfo(ui->destModelInfo, modelId); +} + +void MergeModelsDialog::fillModelInfo(QLabel *label, db_id modelId) +{ + if(!modelId) + { + label->setText(tr("No model set")); + return; + } + + q_getModelInfo.bind(1, modelId); + if(q_getModelInfo.step() != SQLITE_ROW) + { + label->setText(tr("Error")); + q_getModelInfo.reset(); + return; + } + + auto r = q_getModelInfo.getRows(); + QString suffix = r.get(0); + int maxSpeedKmH = r.get(1); + int axes = r.get(2); + RsType type = RsType(r.get(3)); + RsEngineSubType subType = RsEngineSubType(r.get(4)); + + QString typeStr = RsTypeNames::name(type); + if(type == RsType::Engine) + { + typeStr.append(", "); + typeStr.append(RsTypeNames::name(subType)); + } + + if(!suffix.isEmpty()) + { + QString tmp; + tmp.reserve(suffix.size() + 8); + tmp.append(""); //3 char + tmp.append(suffix); + tmp.append(" "); //5 char + suffix = tmp; + } + + label->setText(tr("%1Axes: %2 Max.Speed: %3 km/h
Type: %4") + .arg(suffix) + .arg(axes) + .arg(maxSpeedKmH) + .arg(typeStr)); + + adjustSize(); + + model->autoSuggest(QString()); //Reset query + + q_getModelInfo.reset(); +} + +/* Merge sourceModel in destModel: + * - all rollingstock of model 'sourceModel' will be changed to model 'destModel' + * - if removeSource is true then at the end of the operation 'sourceModel' is deleted from the database + * + * If fails returns -1 + * If succeds returns the number of rollingstock pieces that have been changed + * + * BIG TODO: say we have RS + * Aln773.1251 + * Aln668.1251 + * + * Then we merge Aln773 in Aln668, the result is: + * Aln668.1251 + * Aln668.1251 Two RS identical + * + * We should prevent that by canceling the merge or by changing the number or letting user choose + */ +int MergeModelsDialog::mergeModels(db_id sourceModelId, db_id destModelId, bool removeSource) +{ + if(sourceModelId == destModelId) + return false; //Error: must be different models + + command q_mergeModels(mDb, "UPDATE rs_list SET model_id=? WHERE model_id=?"); + q_mergeModels.bind(1, destModelId); + q_mergeModels.bind(2, sourceModelId); + int ret = q_mergeModels.execute(); + q_mergeModels.reset(); + + if(ret != SQLITE_OK) + { + qDebug() << "Merging Models" << sourceModelId << destModelId; + qDebug() << "DB Error:" << ret << mDb.error_msg() << mDb.extended_error_code(); + return false; + } + + if(removeSource) + { + command q_removeSource(mDb, "DELETE FROM rs_models WHERE id=?"); + q_removeSource.bind(1, sourceModelId); + ret = q_removeSource.execute(); + if(ret != SQLITE_OK) + { + qDebug() << "Removing model" << sourceModelId; + qDebug() << "DB Error:" << ret << mDb.error_msg() << mDb.extended_error_code(); + } + } + + return true; +} diff --git a/src/rollingstock/manager/dialogs/mergemodelsdialog.h b/src/rollingstock/manager/dialogs/mergemodelsdialog.h new file mode 100644 index 0000000..58fe9e6 --- /dev/null +++ b/src/rollingstock/manager/dialogs/mergemodelsdialog.h @@ -0,0 +1,51 @@ +#ifndef MODELMERGEDIALOG_H +#define MODELMERGEDIALOG_H + +#include + +#include "utils/types.h" + +#include + +class CustomCompletionLineEdit; +class RSModelsMatchModel; +class QLabel; + +namespace Ui { +class MergeModelsDialog; +} + +class MergeModelsDialog : public QDialog +{ + Q_OBJECT + +public: + MergeModelsDialog(sqlite3pp::database &db, QWidget *parent = nullptr); + ~MergeModelsDialog(); + +protected: + void done(int r) override; + +private slots: + void sourceModelChanged(db_id modelId); + void destModelChanged(db_id modelId); + +private: + void fillModelInfo(QLabel *label, db_id modelId); + +private: + int mergeModels(db_id sourceModelId, db_id destModelId, bool removeSource); + +private: + Ui::MergeModelsDialog *ui; + + CustomCompletionLineEdit *sourceModelEdit; + CustomCompletionLineEdit *destModelEdit; + + RSModelsMatchModel *model; + + sqlite3pp::database &mDb; + sqlite3pp::query q_getModelInfo; +}; + +#endif // MODELMERGEDIALOG_H diff --git a/src/rollingstock/manager/dialogs/mergemodelsdialog.ui b/src/rollingstock/manager/dialogs/mergemodelsdialog.ui new file mode 100644 index 0000000..dd9f22b --- /dev/null +++ b/src/rollingstock/manager/dialogs/mergemodelsdialog.ui @@ -0,0 +1,148 @@ + + + MergeModelsDialog + + + + 0 + 0 + 600 + 150 + + + + + 0 + 0 + + + + + 300 + 150 + + + + Merge Models + + + + + + + 10 + + + + Remove model A + + + + + + + + 0 + 0 + + + + + 10 + + + + Merge model A in B: all RS of model A will be changed to model B + + + false + + + true + + + + + + + + 10 + + + + Invalid + + + true + + + + + + + + 0 + 0 + + + + + 10 + + + + Invalid + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + MergeModelsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + MergeModelsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/rollingstock/manager/dialogs/mergeownersdialog.cpp b/src/rollingstock/manager/dialogs/mergeownersdialog.cpp new file mode 100644 index 0000000..2961c6b --- /dev/null +++ b/src/rollingstock/manager/dialogs/mergeownersdialog.cpp @@ -0,0 +1,108 @@ +#include "mergeownersdialog.h" +#include "ui_mergeownersdialog.h" + +#include "app/session.h" + +#include "rollingstock/rsownersmatchmodel.h" + +#include +#include "utils/sqldelegate/customcompletionlineedit.h" + +#include + +MergeOwnersDialog::MergeOwnersDialog(database &db, QWidget *parent) : + QDialog(parent), + ui(new Ui::MergeOwnersDialog), + mDb(db) +{ + ui->setupUi(this); + + model = new RSOwnersMatchModel(mDb, this); + sourceOwnerEdit = new CustomCompletionLineEdit(model); + destOwnerEdit = new CustomCompletionLineEdit(model); + + sourceOwnerEdit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + destOwnerEdit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + + int idx = ui->verticalLayout->indexOf(ui->descriptionLabel); + ui->verticalLayout->insertWidget(idx + 1, destOwnerEdit); + ui->verticalLayout->insertWidget(idx + 1, sourceOwnerEdit); + + connect(sourceOwnerEdit, &CustomCompletionLineEdit::editingFinished, this, &MergeOwnersDialog::resetModel); + connect(destOwnerEdit, &CustomCompletionLineEdit::editingFinished, this, &MergeOwnersDialog::resetModel); + + ui->removeSourceCheckBox->setChecked(AppSettings.getRemoveMergedSourceOwner()); +} + +MergeOwnersDialog::~MergeOwnersDialog() +{ + delete ui; +} + +void MergeOwnersDialog::done(int r) +{ + if(r == QDialog::Accepted) + { + db_id sourceOwnerId = 0; + db_id destOwnerId = 0; + QString tmp; + sourceOwnerEdit->getData(sourceOwnerId, tmp); + destOwnerEdit->getData(destOwnerId, tmp); + + //Check input is valid + if(!sourceOwnerId || !destOwnerId || sourceOwnerId == destOwnerId) + { + QMessageBox::warning(this, tr("Invalid Owners"), tr("Owners must not be null and must be different")); + return; //We don't want the dialog to be closed + } + + if(mergeOwners(sourceOwnerId, destOwnerId, ui->removeSourceCheckBox->isChecked())) + { + //Operation succeded, inform user + QMessageBox::information(this, tr("Merging completed"), + tr("Owners merged succesfully.")); + } + else + { + QMessageBox::warning(this, tr("Error while merging"), tr("Some error occurred while merging owners.")); + //Accept dialog to close it, so don't return here + } + } + + QDialog::done(r); +} + +void MergeOwnersDialog::resetModel() +{ + model->autoSuggest(QString()); +} + +bool MergeOwnersDialog::mergeOwners(db_id sourceOwnerId, db_id destOwnerId, bool removeSource) +{ + command q_mergeOwners(mDb, "UPDATE rs_list SET owner_id=? WHERE owner_id=?"); + q_mergeOwners.bind(1, destOwnerId); + q_mergeOwners.bind(2, sourceOwnerId); + int ret = q_mergeOwners.execute(); + q_mergeOwners.reset(); + + if(ret != SQLITE_OK) + { + qDebug() << "Merging Owners" << sourceOwnerId << destOwnerId; + qDebug() << "DB Error:" << ret << mDb.error_msg() << mDb.extended_error_code(); + return false; + } + + if(removeSource) + { + command q_removeSource(mDb, "DELETE FROM rs_owners WHERE id=?"); + q_removeSource.bind(1, sourceOwnerId); + ret = q_removeSource.execute(); + if(ret != SQLITE_OK) + { + qDebug() << "Removing owner" << sourceOwnerId; + qDebug() << "DB Error:" << ret << mDb.error_msg() << mDb.extended_error_code(); + } + } + + return true; +} diff --git a/src/rollingstock/manager/dialogs/mergeownersdialog.h b/src/rollingstock/manager/dialogs/mergeownersdialog.h new file mode 100644 index 0000000..b84205f --- /dev/null +++ b/src/rollingstock/manager/dialogs/mergeownersdialog.h @@ -0,0 +1,48 @@ +#ifndef MERGEOWNERSDIALOG_H +#define MERGEOWNERSDIALOG_H + +#include + +#include "utils/types.h" + +class CustomCompletionLineEdit; +class RSOwnersMatchModel; +class QLabel; + +namespace sqlite3pp { +class database; +} + +namespace Ui { +class MergeOwnersDialog; +} + +class MergeOwnersDialog : public QDialog +{ + Q_OBJECT + +public: + MergeOwnersDialog(sqlite3pp::database &db, QWidget *parent = nullptr); + ~MergeOwnersDialog(); + +protected: + void done(int r) override; + +private slots: + void resetModel(); + +private: + bool mergeOwners(db_id sourceOwnerId, db_id destOwnerId, bool removeSource); + +private: + Ui::MergeOwnersDialog *ui; + + CustomCompletionLineEdit *sourceOwnerEdit; + CustomCompletionLineEdit *destOwnerEdit; + + RSOwnersMatchModel *model; + + sqlite3pp::database &mDb; +}; + +#endif // MERGEOWNERSDIALOG_H diff --git a/src/rollingstock/manager/dialogs/mergeownersdialog.ui b/src/rollingstock/manager/dialogs/mergeownersdialog.ui new file mode 100644 index 0000000..fcecde7 --- /dev/null +++ b/src/rollingstock/manager/dialogs/mergeownersdialog.ui @@ -0,0 +1,94 @@ + + + MergeOwnersDialog + + + + 0 + 0 + 400 + 145 + + + + + 400 + 145 + + + + Merge Owners + + + + + + + 10 + + + + Merge owner A in B: all RS of owner A will be changed to owner B + + + + + + + + 10 + + + + Remove Owner A + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + MergeOwnersDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + MergeOwnersDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/rollingstock/manager/rollingstockmanager.cpp b/src/rollingstock/manager/rollingstockmanager.cpp new file mode 100644 index 0000000..a59a401 --- /dev/null +++ b/src/rollingstock/manager/rollingstockmanager.cpp @@ -0,0 +1,673 @@ +#include "rollingstockmanager.h" + +#include "app/session.h" + +#include "delegates/rstypedelegate.h" +#include "delegates/rsnumberdelegate.h" + +#include +#include +#include +#include +#include +#include + +#include "viewmanager/viewmanager.h" + +#include "../importer/rsimportwizard.h" + +#include "dialogs/mergemodelsdialog.h" +#include "dialogs/mergeownersdialog.h" + +#include "utils/sqldelegate/modelpageswitcher.h" +#include "../rollingstocksqlmodel.h" +#include "../rsmodelssqlmodel.h" +#include "../rsownerssqlmodel.h" +#include +#include +#include +#include "utils/sqldelegate/sqlfkfielddelegate.h" +#include "utils/sqldelegate/chooseitemdlg.h" +#include "utils/sqldelegate/isqlfkmatchmodel.h" +#include "../rsmatchmodelfactory.h" + +#include "utils/rs_types_names.h" + +#include "utils/spinbox/spinboxeditorfactory.h" + +#include +#include + +RollingStockManager::RollingStockManager(QWidget *parent) : + QWidget(parent), + oldCurrentTab(RollingstockTab), + clearModelTimers{0, 0, 0}, + m_readOnly(false), + windowConnected(false) +{ + QVBoxLayout *lay = new QVBoxLayout(this); + lay->setContentsMargins(2, 2, 2, 2); + tabWidget = new QTabWidget; + lay->addWidget(tabWidget); + setupPages(); + setReadOnly(false); + + tabWidget->setCurrentIndex(RollingstockTab); + connect(tabWidget, &QTabWidget::currentChanged, this, &RollingStockManager::updateModels); + + setWindowTitle(tr("Rollingstock Manager")); + resize(700, 500); +} + +void RollingStockManager::setupPages() +{ + //RollingStock Page + QWidget *rollingstockTab = new QWidget; + tabWidget->addTab(rollingstockTab, RsTypeNames::tr("Rollingstock")); + QVBoxLayout *rsLay = new QVBoxLayout(rollingstockTab); + rsLay->setContentsMargins(2, 2, 2, 2); + + rsToolBar = new QToolBar; + rsLay->addWidget(rsToolBar); + + actionNewRs = rsToolBar->addAction(tr("New Rollingstock"), + this, &RollingStockManager::onNewRs); + actionDeleteRs = rsToolBar->addAction(tr("Remove"), + this, &RollingStockManager::onRemoveRs); + + actionViewRSPlan = rsToolBar->addAction(tr("View Plan"), + this, &RollingStockManager::onViewRSPlan); + QMenu *actionRsPlanMenu = new QMenu; + actionViewRSPlanSearch = actionRsPlanMenu->addAction(tr("Search rollingstock item"), + this, &RollingStockManager::onViewRSPlanSearch); + actionViewRSPlan->setMenu(actionRsPlanMenu); + + actionDeleteAllRs = rsToolBar->addAction(tr("Delete All Rollingstock"), + this, &RollingStockManager::onRemoveAllRs); + + rsToolBar->addAction(tr("Import"), this, &RollingStockManager::onImportRS); + rsToolBar->addAction(tr("Session Summary"), this, &RollingStockManager::showSessionRSViewer); + + rsView = new QTableView; + rsLay->addWidget(rsView); + + rsSQLModel = new RollingstockSQLModel(Session->m_Db, this); + rsView->setModel(rsSQLModel); + + auto ps = new ModelPageSwitcher(false, this); + rsLay->addWidget(ps); + ps->setModel(rsSQLModel); + //Custom colun sorting + //NOTE: leave disconnect() in the old SIGLAL()/SLOT() version in order to work + QHeaderView *header = rsView->horizontalHeader(); + disconnect(header, SIGNAL(sectionPressed(int)), rsView, SLOT(selectColumn(int))); + disconnect(header, SIGNAL(sectionEntered(int)), rsView, SLOT(_q_selectColumn(int))); + connect(header, &QHeaderView::sectionClicked, this, [this, header](int section) + { + rsSQLModel->setSortingColumn(section); + header->setSortIndicator(rsSQLModel->getSortingColumn(), Qt::AscendingOrder); + }); + header->setSortIndicatorShown(true); + header->setSortIndicator(rsSQLModel->getSortingColumn(), Qt::AscendingOrder); + + + //Models Page + QWidget *modelsTab = new QWidget; + tabWidget->addTab(modelsTab, RsTypeNames::tr("Models")); + QVBoxLayout *modelsLay = new QVBoxLayout(modelsTab); + modelsLay->setContentsMargins(2, 2, 2, 2); + + modelToolBar = new QToolBar; + modelsLay->addWidget(modelToolBar); + + actionNewModel = modelToolBar->addAction(tr("New Model"), + this, &RollingStockManager::onNewRsModel); + actionDeleteModel = modelToolBar->addAction(tr("Remove"), + this, &RollingStockManager::onRemoveRsModel); + actionMergeModels = modelToolBar->addAction(tr("Merge Models"), + this, &RollingStockManager::onMergeModels); + actionNewModelWithSuffix = modelToolBar->addAction(tr("New with suffix"), + this, &RollingStockManager::onNewRsModelWithDifferentSuffixFromCurrent); + QMenu *actionModelSuffixMenu = new QMenu; + actionNewModelWithSuffixSearch = actionModelSuffixMenu->addAction(tr("Search rollingstock model"), + this, &RollingStockManager::onNewRsModelWithDifferentSuffixFromSearch); + actionNewModelWithSuffix->setMenu(actionModelSuffixMenu); + + actionDeleteAllRsModels = modelToolBar->addAction(tr("Delete All Models"), + this, &RollingStockManager::onRemoveAllRsModels); + + rsModelsView = new QTableView; + modelsLay->addWidget(rsModelsView); + modelsSQLModel = new RSModelsSQLModel(Session->m_Db, this); + rsModelsView->setModel(modelsSQLModel); + + ps = new ModelPageSwitcher(false, this); + modelsLay->addWidget(ps); + ps->setModel(modelsSQLModel); + //Custom colun sorting + //NOTE: leave disconnect() in the old SIGLAL()/SLOT() version in order to work + header = rsModelsView->horizontalHeader(); + disconnect(header, SIGNAL(sectionPressed(int)), rsModelsView, SLOT(selectColumn(int))); + disconnect(header, SIGNAL(sectionEntered(int)), rsModelsView, SLOT(_q_selectColumn(int))); + connect(header, &QHeaderView::sectionClicked, this, [this, header](int section) + { + modelsSQLModel->setSortingColumn(section); + header->setSortIndicator(modelsSQLModel->getSortingColumn(), Qt::AscendingOrder); + }); + header->setSortIndicatorShown(true); + header->setSortIndicator(modelsSQLModel->getSortingColumn(), Qt::AscendingOrder); + + //Owners Page + QWidget *ownersTab = new QWidget; + tabWidget->addTab(ownersTab, RsTypeNames::tr("Owners")); + QVBoxLayout *ownersLay = new QVBoxLayout(ownersTab); + ownersLay->setContentsMargins(2, 2, 2, 2); + + ownersToolBar = new QToolBar; + ownersLay->addWidget(ownersToolBar); + + actionNewOwner = ownersToolBar->addAction(tr("New Owner"), this, + &RollingStockManager::onNewOwner); + actionDeleteOwner = ownersToolBar->addAction(tr("Remove"), this, + &RollingStockManager::onRemoveOwner); + actionMergeOwners = ownersToolBar->addAction(tr("Merge Owners"), this, + &RollingStockManager::onMergeOwners); + actionDeleteAllRsOwners = ownersToolBar->addAction(tr("Delete All Owners"), + this, &RollingStockManager::onRemoveAllRsOwners); + + ownersView = new QTableView; + ownersLay->addWidget(ownersView); + + ownersSQLModel = new RSOwnersSQLModel(Session->m_Db, this); + ownersView->setModel(ownersSQLModel); + ps = new ModelPageSwitcher(false, this); + ownersLay->addWidget(ps); + ps->setModel(ownersSQLModel); + + //Setup Delegates + //auto modelDelegate = new RSModelDelegate(modelsModel, this); + auto modelDelegate = new SqlFKFieldDelegate(new RSMatchModelFactory(ModelModes::Models, Session->m_Db, this), rsSQLModel, this); + rsView->setItemDelegateForColumn(RollingstockSQLModel::Model, modelDelegate); + + //auto ownerDelegate = new RSOwnerDelegate(ownersModel, this); + auto ownerDelegate = new SqlFKFieldDelegate(new RSMatchModelFactory(ModelModes::Owners, Session->m_Db, this), rsSQLModel, this); + rsView->setItemDelegateForColumn(RollingstockSQLModel::Owner, ownerDelegate); + + auto numberDelegate = new RsNumberDelegate(this); + rsView->setItemDelegateForColumn(RollingstockSQLModel::Number, numberDelegate); + + auto rsSpeedDelegate = new QStyledItemDelegate(this); + speedSpinFactory = new SpinBoxEditorFactory; + speedSpinFactory->setRange(1, 999); + speedSpinFactory->setSuffix(" km/h"); + speedSpinFactory->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + rsSpeedDelegate->setItemEditorFactory(speedSpinFactory); + rsModelsView->setItemDelegateForColumn(RSModelsSQLModel::MaxSpeed, rsSpeedDelegate); + + auto rsAxesDelegate = new QStyledItemDelegate(this); + axesSpinFactory = new SpinBoxEditorFactory; + axesSpinFactory->setRange(2, 999); + axesSpinFactory->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + rsAxesDelegate->setItemEditorFactory(axesSpinFactory); + rsModelsView->setItemDelegateForColumn(RSModelsSQLModel::Axes, rsAxesDelegate); + + auto rsTypeDelegate = new RSTypeDelegate(false, this); + rsModelsView->setItemDelegateForColumn(RSModelsSQLModel::TypeCol, rsTypeDelegate); + + auto rsSubTypeDelegate = new RSTypeDelegate(true, this); + rsModelsView->setItemDelegateForColumn(RSModelsSQLModel::SubTypeCol, rsSubTypeDelegate); + + //Setup actions + editActGroup = new QActionGroup(this); + editActGroup->addAction(actionNewRs); + editActGroup->addAction(actionDeleteRs); + editActGroup->addAction(actionDeleteAllRs); + editActGroup->addAction(actionNewModel); + editActGroup->addAction(actionNewModelWithSuffix); + editActGroup->addAction(actionNewModelWithSuffixSearch); + editActGroup->addAction(actionDeleteModel); + editActGroup->addAction(actionDeleteAllRsModels); + editActGroup->addAction(actionMergeModels); + editActGroup->addAction(actionNewOwner); + editActGroup->addAction(actionDeleteOwner); + editActGroup->addAction(actionDeleteAllRsOwners); + editActGroup->addAction(actionMergeOwners); +} + +RollingStockManager::~RollingStockManager() +{ + delete speedSpinFactory; + delete axesSpinFactory; + + for(int i = 0; i < NTabs; i++) + { + if(clearModelTimers[i] > 0) + { + killTimer(clearModelTimers[i]); + clearModelTimers[i] = 0; + } + } +} + +void RollingStockManager::setReadOnly(bool readOnly) +{ + if(m_readOnly == readOnly) + return; + + m_readOnly = readOnly; + + editActGroup->setDisabled(m_readOnly); + + if(m_readOnly) + { + rsView->setEditTriggers(QAbstractItemView::NoEditTriggers); + rsModelsView->setEditTriggers(QAbstractItemView::NoEditTriggers); + ownersView->setEditTriggers(QAbstractItemView::NoEditTriggers); + } + else + { + rsView->setEditTriggers(QAbstractItemView::DoubleClicked); + rsModelsView->setEditTriggers(QAbstractItemView::DoubleClicked); + ownersView->setEditTriggers(QAbstractItemView::DoubleClicked); + } +} + +void RollingStockManager::updateModels() +{ + int curTab = tabWidget->currentIndex(); + + if(clearModelTimers[curTab] > 0) + { + //This page was already cached, stop it from clearing + killTimer(clearModelTimers[curTab]); + } + else if(clearModelTimers[curTab] == ModelCleared) + { + //This page wasn't already cached + switch (curTab) + { + case RollingstockTab: + { + rsSQLModel->refreshData(); + break; + } + case ModelsTab: + { + modelsSQLModel->refreshData(); + break; + } + case OwnersTab: + { + ownersSQLModel->refreshData(); + break; + } + } + } + clearModelTimers[curTab] = ModelLoaded; + + //Now start timer to clear old current page if not already done + if(oldCurrentTab != curTab && clearModelTimers[oldCurrentTab] == ModelLoaded) //Wait 10 seconds and then clear cache + { + clearModelTimers[oldCurrentTab] = startTimer(ClearModelTimeout, Qt::VeryCoarseTimer); + } + + oldCurrentTab = curTab; +} + +void RollingStockManager::visibilityChanged(int v) +{ + if(v == QWindow::Minimized || v == QWindow::Hidden) + { + //If the window is minimized start timer to clear model cache of current tab + //The other tabs already have been cleared or are waiting with their timers + if(clearModelTimers[oldCurrentTab] == ModelLoaded) + { + clearModelTimers[oldCurrentTab] = startTimer(ClearModelTimeout, Qt::VeryCoarseTimer); + } + }else{ + updateModels(); + } +} + +void RollingStockManager::importRS(bool resume, QWidget *parent) +{ + RSImportWizard w(resume, parent); + w.exec(); +} + +void RollingStockManager::onViewRSPlan() +{ + //TODO: use also a search if requested RS is not in current page + QModelIndex idx = rsView->currentIndex(); + if(!idx.isValid()) + return; + + db_id rsId = rsSQLModel->getIdAtRow(idx.row()); + if(!rsId) + return; + Session->getViewManager()->requestRSInfo(rsId); +} + +void RollingStockManager::onViewRSPlanSearch() +{ + //TODO: add search dialog also for deleting owners/models/RS items. + + RSMatchModelFactory factory(ModelModes::Rollingstock, Session->m_Db, this); + std::unique_ptr matchModel; + matchModel.reset(factory.createModel()); + ChooseItemDlg dlg(matchModel.get(), this); + dlg.setDescription(tr("Please choose a rollingstock item")); + dlg.setPlaceholder(tr("[model][.][number][:owner]")); + + if(dlg.exec() != QDialog::Accepted) + return; + + Session->getViewManager()->requestRSInfo(dlg.getItemId()); +} + +void RollingStockManager::onNewRs() +{ + if(m_readOnly) + return; + + QString errText; + int row = 0; + db_id rsId = rsSQLModel->addRSItem(&row, &errText); + if(!rsId) + { + QMessageBox::warning(this, tr("Error adding rollingstock piece"), errText); + return; + } +} + +void RollingStockManager::onRemoveRs() +{ + if(m_readOnly) + return; + + QModelIndex idx = rsView->currentIndex(); + if(!idx.isValid()) + return; + + if(!rsSQLModel->removeRSItemAt(idx.row())) + { + //ERRORMSG: display error + return; + } +} + +void RollingStockManager::onRemoveAllRs() +{ + int ret = QMessageBox::question(this, tr("Delete All Rollingstock?"), + tr("Are you really sure you want to delete all rollingstock from this session?\n" + "NOTE: this will not erease model and owners, just rollingstock pieces.")); + if(ret == QMessageBox::Yes) + { + if(!rsSQLModel->removeAllRS()) + { + QMessageBox::warning(this, tr("Error"), + tr("Failed to remove rollingstock.\n" + "Make sure there are no more couplings in this session.\n" + "NOTE: you can remove all jobs at once from the Jobs Manager.")); + } + } +} + +void RollingStockManager::onNewRsModel() +{ + if(m_readOnly) + return; + + int row = 0; + + QString errText; + db_id modelId = modelsSQLModel->addRSModel(&row, 0, QString(), &errText); + if(!modelId) + { + QMessageBox::warning(this, tr("Error adding model"), errText); + return; + } + + QModelIndex idx = modelsSQLModel->index(row, RSModelsSQLModel::Name); + + rsModelsView->setCurrentIndex(idx); + rsModelsView->scrollTo(idx); + rsModelsView->edit(idx); //TODO: delay call until row is fetched!!! Like save it and wait for a signal from model +} + +void RollingStockManager::onNewRsModelWithDifferentSuffixFromCurrent() +{ + if(m_readOnly) + return; + + QModelIndex idx = rsModelsView->currentIndex(); + if(!idx.isValid()) + return; + + db_id modelId = modelsSQLModel->getModelIdAtRow(idx.row()); + if(modelId) + { + QString errMsg; + if(!createRsModelWithDifferentSuffix(modelId, errMsg, this)) + { + QMessageBox::warning(this, tr("Error"), errMsg); + } + } +} + +void RollingStockManager::onNewRsModelWithDifferentSuffixFromSearch() +{ + if(m_readOnly) + return; + + RSMatchModelFactory factory(ModelModes::Models, Session->m_Db, this); + std::unique_ptr matchModel; + matchModel.reset(factory.createModel()); + ChooseItemDlg dlg(matchModel.get(), this); + dlg.setDescription(tr("Please choose a rollingstock model")); + dlg.setPlaceholder(tr("Model")); + dlg.setCallback([this, &dlg](db_id modelId, QString &errMsg) -> bool + { + if(!modelId) + { + errMsg = tr("You must select a valid rollingstock model."); + return false; + } + + return createRsModelWithDifferentSuffix(modelId, errMsg, &dlg); + }); + + if(dlg.exec() == QDialog::Accepted) + { + //TODO: select and edit the new item + } +} + +bool RollingStockManager::createRsModelWithDifferentSuffix(db_id sourceModelId, QString &errMsg, QWidget *w) +{ + QInputDialog dlg(w); + dlg.setLabelText(tr("Please choose an unique suffix for this model, or leave empty")); + dlg.setWindowTitle(tr("Choose Suffix")); + dlg.setInputMode(QInputDialog::TextInput); + + if(dlg.exec() == QDialog::Accepted) + return modelsSQLModel->addRSModel(nullptr, sourceModelId, dlg.textValue(), &errMsg); + return true; //Abort without errors +} + +void RollingStockManager::onRemoveRsModel() +{ + if(m_readOnly) + return; + + QModelIndex idx = rsModelsView->currentIndex(); + if(!idx.isValid()) + return; + + if(!modelsSQLModel->removeRSModelAt(idx.row())) + { + //ERRORMSG: + } +} + +void RollingStockManager::onRemoveAllRsModels() +{ + int ret = QMessageBox::question(this, tr("Delete All Rollingstock Models?"), + tr("Are you really sure you want to delete all rollingstock models from this session?\n" + "NOTE: this can be done only if there are no rollingstock pieces in this session.")); + if(ret == QMessageBox::Yes) + { + if(!modelsSQLModel->removeAllRSModels()) + { + QMessageBox::warning(this, tr("Error"), + tr("Failed to remove rollingstock models.\n" + "Make sure there are no more rollingstock pieces in this session.\n" + "NOTE: you can remove all rollinstock pieces at once from the Rollingstock tab.")); + } + } +} + +void RollingStockManager::onNewOwner() +{ + if(m_readOnly) + return; + + int row = 0; + if(!ownersSQLModel->addRSOwner(&row)) + { + //ERRORMSG: display + return; + } + + QModelIndex idx = ownersSQLModel->index(row, 0); + + ownersView->setCurrentIndex(idx); + ownersView->scrollTo(idx); + ownersView->edit(idx); +} + +void RollingStockManager::onRemoveOwner() +{ + if(m_readOnly) + return; + + QModelIndex idx = ownersView->currentIndex(); + if(!idx.isValid()) + return; + + if(!ownersSQLModel->removeRSOwnerAt(idx.row())) + { + //ERRORMSG: display error + } +} + +void RollingStockManager::onRemoveAllRsOwners() +{ + int ret = QMessageBox::question(this, tr("Delete All Rollingstock Owners?"), + tr("Are you really sure you want to delete all rollingstock owners from this session?\n" + "NOTE: this can be done only if there are no rollingstock pieces in this session.")); + if(ret == QMessageBox::Yes) + { + if(!ownersSQLModel->removeAllRSOwners()) + { + QMessageBox::warning(this, tr("Error"), + tr("Failed to remove rollingstock owners.\n" + "Make sure there are no more rollingstock pieces in this session.\n" + "NOTE: you can remove all rollingstock pieces at once from the Rollingstock tab.")); + } + } +} + +void RollingStockManager::onMergeModels() +{ + if(m_readOnly) + return; + + MergeModelsDialog dlg(Session->m_Db, this); + dlg.exec(); + + if(clearModelTimers[ModelsTab] == ModelLoaded) + { + modelsSQLModel->refreshData(); + } + if(clearModelTimers[RollingstockTab] == ModelLoaded) + { + rsSQLModel->refreshData(); + rsSQLModel->clearCache(); + } +} + +void RollingStockManager::onMergeOwners() +{ + if(m_readOnly) + return; + + MergeOwnersDialog dlg(Session->m_Db, this); + dlg.exec(); + + if(clearModelTimers[OwnersTab] == ModelLoaded) + { + ownersSQLModel->refreshData(); + } + if(clearModelTimers[RollingstockTab] == ModelLoaded) + { + rsSQLModel->refreshData(); + rsSQLModel->clearCache(); + } +} + +void RollingStockManager::onImportRS() +{ + importRS(false, this); + + rsSQLModel->refreshData(); + modelsSQLModel->refreshData(); + ownersSQLModel->refreshData(); +} + +void RollingStockManager::showSessionRSViewer() +{ + Session->getViewManager()->showSessionStartEndRSViewer(); +} + +void RollingStockManager::timerEvent(QTimerEvent *e) +{ + if(e->timerId() == clearModelTimers[RollingstockTab]) + { + rsSQLModel->clearCache(); + killTimer(e->timerId()); + clearModelTimers[RollingstockTab] = ModelCleared; + return; + } + else if(e->timerId() == clearModelTimers[ModelsTab]) + { + modelsSQLModel->clearCache(); + killTimer(e->timerId()); + clearModelTimers[ModelsTab] = ModelCleared; + return; + } + else if(e->timerId() == clearModelTimers[OwnersTab]) + { + ownersSQLModel->clearCache(); + killTimer(e->timerId()); + clearModelTimers[OwnersTab] = ModelCleared; + return; + } + + QWidget::timerEvent(e); +} + +void RollingStockManager::showEvent(QShowEvent *e) +{ + if(!windowConnected) + { + QWindow *w = windowHandle(); + if(w) + { + windowConnected = true; + connect(w, &QWindow::visibilityChanged, this, &RollingStockManager::visibilityChanged); + updateModels(); + } + } + QWidget::showEvent(e); +} diff --git a/src/rollingstock/manager/rollingstockmanager.h b/src/rollingstock/manager/rollingstockmanager.h new file mode 100644 index 0000000..070b51a --- /dev/null +++ b/src/rollingstock/manager/rollingstockmanager.h @@ -0,0 +1,132 @@ +#ifndef ROLLINGSTOCKMANAGER_H +#define ROLLINGSTOCKMANAGER_H + +#include + +#include "utils/types.h" + +class QTableView; +class QToolBar; +class QTabWidget; + +class RollingstockSQLModel; +class RSModelsSQLModel; +class RSOwnersSQLModel; + +class SpinBoxEditorFactory; + +class QActionGroup; + +class RollingStockManager : public QWidget +{ + Q_OBJECT + +public: + typedef enum + { + RollingstockTab = 0, + ModelsTab, + OwnersTab, + NTabs + } Tabs; + + typedef enum + { + ModelCleared = 0, + ModelLoaded = -1 + } ModelState; + + enum { ClearModelTimeout = 5000 }; // 5 seconds + + explicit RollingStockManager(QWidget *parent = nullptr); + ~RollingStockManager(); + + void setReadOnly(bool readOnly = true); + + static void importRS(bool resume, QWidget *parent); + +private slots: + void updateModels(); + void visibilityChanged(int v); + + void onViewRSPlan(); + void onViewRSPlanSearch(); + + void onNewRs(); + void onRemoveRs(); + void onRemoveAllRs(); + + void onNewRsModel(); + void onNewRsModelWithDifferentSuffixFromCurrent(); + void onNewRsModelWithDifferentSuffixFromSearch(); + void onRemoveRsModel(); + void onRemoveAllRsModels(); + + void onNewOwner(); + void onRemoveOwner(); + void onRemoveAllRsOwners(); + + void onMergeModels(); + void onMergeOwners(); + + void onImportRS(); + + void showSessionRSViewer(); + +protected: + virtual void timerEvent(QTimerEvent *e) override; + virtual void showEvent(QShowEvent *e) override; + +private: + void setupPages(); + bool createRsModelWithDifferentSuffix(db_id sourceModelId, QString &errMsg, QWidget *w); + +private: + QTabWidget *tabWidget; + + QToolBar *rsToolBar; + QToolBar *modelToolBar; + QToolBar *ownersToolBar; + + QActionGroup *editActGroup; + + QAction *actionNewRs; + QAction *actionDeleteRs; + QAction *actionDeleteAllRs; + + QAction *actionNewModel; + QAction *actionNewModelWithSuffix; + QAction *actionNewModelWithSuffixSearch; + QAction *actionDeleteModel; + QAction *actionDeleteAllRsModels; + + QAction *actionNewOwner; + QAction *actionDeleteOwner; + QAction *actionDeleteAllRsOwners; + + QAction *actionMergeModels; + QAction *actionMergeOwners; + + QAction *actionViewRSPlan; + QAction *actionViewRSPlanSearch; + + QTableView *rsView; + QTableView *rsModelsView; + QTableView *ownersView; + + SpinBoxEditorFactory *speedSpinFactory; + SpinBoxEditorFactory *axesSpinFactory; + + RollingstockSQLModel *rsSQLModel; + RSModelsSQLModel *modelsSQLModel; + RSOwnersSQLModel *ownersSQLModel; + + + int oldCurrentTab; + int clearModelTimers[NTabs]; + + bool m_readOnly; + bool windowConnected; +}; + +#endif // ROLLINGSTOCKMANAGER_H diff --git a/src/rollingstock/rollingstockmatchmodel.cpp b/src/rollingstock/rollingstockmatchmodel.cpp new file mode 100644 index 0000000..de2a150 --- /dev/null +++ b/src/rollingstock/rollingstockmatchmodel.cpp @@ -0,0 +1,195 @@ +#include "rollingstockmatchmodel.h" + +#include "utils/rs_utils.h" + +RollingstockMatchModel::RollingstockMatchModel(database &db, QObject *parent) : + ISqlFKMatchModel(parent), + mDb(db), + q_getMatches(mDb) +{ + regExp.setPattern("(?[\\w\\s-]*)\\s*\\.?\\s*(?\\d*)\\s*(:(?[\\w\\s-]+)?)?\\w*"); + regExp.optimize(); + q_getMatches.prepare("SELECT rs_list.id,rs_list.number,rs_models.name,rs_models.suffix,rs_models.type,rs_owners.name," + "(CASE WHEN rs_list.number LIKE ?1 THEN 2 ELSE 0 END +" + " CASE WHEN rs_models.name LIKE ?2 THEN 1 ELSE 0 END +" + " CASE WHEN rs_models.suffix LIKE ?2 THEN 1 ELSE 0 END +" + " CASE WHEN rs_owners.name LIKE ?3 THEN 3 ELSE 0 END) AS s" + " FROM rs_list" + " JOIN rs_models ON rs_models.id=rs_list.model_id" + " JOIN rs_owners ON rs_owners.id=rs_list.owner_id" + " ORDER BY s DESC LIMIT " QT_STRINGIFY(MaxMatchItems + 1)); + //FIXME: //FIXME: non funziona bene, i risultati sembrano casuali +} + +int RollingstockMatchModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant RollingstockMatchModel::data(const QModelIndex &idx, int role) const +{ + if (!idx.isValid() || idx.row() >= size) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + { + if(isEmptyRow(idx.row())) + { + return idx.column() == NumberCol ? ISqlFKMatchModel::tr("Empty") : QVariant(); + } + else if(isEllipsesRow(idx.row())) + { + return ellipsesString; + } + + const RSItem& item = items[idx.row()]; + + switch (idx.column()) + { + case ModelCol: + return item.modelName; + case NumberCol: + return rs_utils::formatNum(item.type, item.number); + case SuffixCol: + return item.modelSuffix; + case OwnerCol: + return item.ownerName; + } + break; + } + case Qt::FontRole: + { + if(isEmptyRow(idx.row()) || idx.column() == OwnerCol) + { + return boldFont(); + } + break; + } + } + + return QVariant(); +} + +void RollingstockMatchModel::autoSuggest(const QString &text) +{ + QRegularExpressionMatch match = regExp.match(text); + + QString tmp = match.captured("model").toUtf8(); + model.clear(); + if(!tmp.isEmpty()) + { + model.reserve(tmp.size() + 2); + model.append('%'); + model.append(tmp.toUtf8()); + model.append('%'); + } + + tmp = match.captured("num").toUtf8(); + number.clear(); + if(!tmp.isEmpty()) + { + number.reserve(tmp.size() + 2); + number.append('%'); + number.append(tmp.toUtf8()); + number.append('%'); + } + + tmp = match.captured("owner").toUtf8(); + owner.clear(); + if(!tmp.isEmpty()) + { + owner.reserve(tmp.size() + 2); + owner.append('%'); + owner.append(tmp.toUtf8()); + owner.append('%'); + } + + refreshData(); +} + +void RollingstockMatchModel::refreshData() +{ + if(!mDb.db()) + return; + + beginResetModel(); + + if(number.isEmpty()) + sqlite3_bind_null(q_getMatches.stmt(), 1); + else + sqlite3_bind_text(q_getMatches.stmt(), 1, number, number.size(), SQLITE_STATIC); + + if(model.isEmpty()) + sqlite3_bind_null(q_getMatches.stmt(), 2); + else + sqlite3_bind_text(q_getMatches.stmt(), 2, model, model.size(), SQLITE_STATIC); + + if(owner.isEmpty()) + sqlite3_bind_null(q_getMatches.stmt(), 3); + else + sqlite3_bind_text(q_getMatches.stmt(), 3, owner, owner.size(), SQLITE_STATIC); + + + int i = 0; + int ret = SQLITE_OK; + while ((ret = q_getMatches.step() == SQLITE_ROW) && i < MaxMatchItems) + { + auto r = q_getMatches.getRows(); + RSItem &item = items[i]; + item.rsId = r.get(0); + item.number = r.get(1); + item.modelName = r.get(2); + item.modelSuffix = r.get(3); + item.type = RsType(r.get(4)); + item.ownerName = r.get(5); + i++; + } + + size = i + 1; //Items + Empty + + if(ret == SQLITE_ROW) + { + //There would be still rows, show Ellipses + size++; //Items + Empty + Ellispses + } + + q_getMatches.reset(); + endResetModel(); + + emit resultsReady(true); +} + +QString RollingstockMatchModel::getName(db_id id) const +{ + if(!mDb.db()) + return QString(); + + query q(mDb, "SELECT rs_list.number,rs_models.name,rs_models.suffix,rs_models.type" + " FROM rs_list JOIN rs_models ON rs_models.id=rs_list.model_id" + " WHERE rs_list.id=?"); + q.bind(1, id); + if(q.step() != SQLITE_ROW) + return QString(); + + int num = sqlite3_column_int(q.stmt(), 0); + int modelNameLen = sqlite3_column_bytes(q.stmt(), 1); + const char *modelName = reinterpret_cast(sqlite3_column_text(q.stmt(), 1)); + + int modelSuffixLen = sqlite3_column_bytes(q.stmt(), 2); + const char *modelSuffix = reinterpret_cast(sqlite3_column_text(q.stmt(), 2)); + RsType type = RsType(sqlite3_column_int(q.stmt(), 3)); + + return rs_utils::formatNameRef(modelName, modelNameLen, num, modelSuffix, modelSuffixLen, type); +} + +db_id RollingstockMatchModel::getIdAtRow(int row) const +{ + return items[row].rsId; +} + +QString RollingstockMatchModel::getNameAtRow(int row) const +{ + return rs_utils::formatName(items[row].modelName, items[row].number, items[row].modelSuffix, items[row].type); +} diff --git a/src/rollingstock/rollingstockmatchmodel.h b/src/rollingstock/rollingstockmatchmodel.h new file mode 100644 index 0000000..f9fe012 --- /dev/null +++ b/src/rollingstock/rollingstockmatchmodel.h @@ -0,0 +1,59 @@ +#ifndef ROLLINGSTOCKMATCHMODEL_H +#define ROLLINGSTOCKMATCHMODEL_H + +#include "utils/sqldelegate/isqlfkmatchmodel.h" + +#include "utils/types.h" + +#include +using namespace sqlite3pp; + +#include + +class RollingstockMatchModel : public ISqlFKMatchModel +{ + Q_OBJECT + +public: + enum Columns + { + ModelCol = 0, + NumberCol, + SuffixCol, + OwnerCol, + NCols + }; + RollingstockMatchModel(database &db, QObject *parent = nullptr); + + // QAbstractTableModel + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override; + + // ISqlFKMatchModel + void autoSuggest(const QString& text) override; + virtual void refreshData() override; + QString getName(db_id id) const override; + + db_id getIdAtRow(int row) const override; + QString getNameAtRow(int row) const override; + +private: + struct RSItem + { + db_id rsId; + QString modelName; + QString modelSuffix; + QString ownerName; + int number; + RsType type; + }; + RSItem items[MaxMatchItems]; + + database &mDb; + query q_getMatches; + + QRegularExpression regExp; + QByteArray model,owner,number; +}; + +#endif // ROLLINGSTOCKMATCHMODEL_H diff --git a/src/rollingstock/rollingstocksqlmodel.cpp b/src/rollingstock/rollingstocksqlmodel.cpp new file mode 100644 index 0000000..091eb69 --- /dev/null +++ b/src/rollingstock/rollingstocksqlmodel.cpp @@ -0,0 +1,804 @@ +#include "rollingstocksqlmodel.h" +#include "app/session.h" + +#include +#include + +#include +using namespace sqlite3pp; + +#include "utils/worker_event_types.h" +#include "utils/model_roles.h" +#include "utils/rs_types_names.h" +#include "utils/rs_utils.h" + +#include + +class RollingstockModelResultEvent : public QEvent +{ +public: + static constexpr Type _Type = Type(CustomEvents::RollingstockModelResult); + inline RollingstockModelResultEvent() : QEvent(_Type) {} + + QVector items; + int firstRow; +}; + +//ERRORMSG: show other errors +static constexpr char +errorRSInUseCannotDelete[] = QT_TRANSLATE_NOOP("RollingstockSQLModel", + "Rollingstock item %1 is used in some jobs so it cannot be removed.
" + "If you wish to remove it, please first remove it from its jobs."); + +RollingstockSQLModel::RollingstockSQLModel(sqlite3pp::database &db, QObject *parent) : + IPagedItemModel(500, db, parent), + cacheFirstRow(0), + firstPendingRow(-BatchSize) +{ + sortColumn = Model; +} + +bool RollingstockSQLModel::event(QEvent *e) +{ + if(e->type() == RollingstockModelResultEvent::_Type) + { + RollingstockModelResultEvent *ev = static_cast(e); + ev->setAccepted(true); + + handleResult(ev->items, ev->firstRow); + + return true; + } + + return QAbstractTableModel::event(e); +} + +QVariant RollingstockSQLModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(role == Qt::DisplayRole) + { + if(orientation == Qt::Horizontal) + { + switch (section) + { + case Model: + return RsTypeNames::tr("Model"); + case Number: + return RsTypeNames::tr("Number"); + case Suffix: + return RsTypeNames::tr("Suffix"); + case Owner: + return RsTypeNames::tr("Owner"); + case TypeCol: + return RsTypeNames::tr("Type"); + default: + break; + } + } + else + { + return section + curPage * ItemsPerPage + 1; + } + } + return IPagedItemModel::headerData(section, orientation, role); +} + +int RollingstockSQLModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : curItemCount; +} + +int RollingstockSQLModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant RollingstockSQLModel::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 Model: + return item.modelName; + case Number: + return rs_utils::formatNum(item.type, item.number); + case Suffix: + return item.modelSuffix; + case Owner: + return item.ownerName; + case TypeCol: + return RsTypeNames::name(item.type); + } + + break; + } + case Qt::TextAlignmentRole: + { + if(idx.column() == Number) + { + return Qt::AlignVCenter + Qt::AlignRight; + } + break; + } + case RS_NUMBER: + return item.number; + case RS_IS_ENGINE: + return item.type == RsType::Engine; + } + + return QVariant(); +} + +bool RollingstockSQLModel::setData(const QModelIndex &idx, const QVariant &value, int role) +{ + const int row = idx.row(); + if(!idx.isValid() || row >= curItemCount || idx.column() >= NCols || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + RSItem &item = cache[row - cacheFirstRow]; + QModelIndex first = idx; + QModelIndex last = idx; + + if(role == RS_NUMBER) + { + bool ok = false; + int number = value.toInt(&ok); + if(!ok || !setNumber(item, number)) + return false; + } + + emit dataChanged(first, last); + return true; +} + +Qt::ItemFlags RollingstockSQLModel::flags(const QModelIndex &idx) const +{ + if (!idx.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren; + if(idx.row() < cacheFirstRow || idx.row() >= cacheFirstRow + cache.size()) + return f; //Not fetched yet + + if(idx.column() != Suffix && idx.column() != TypeCol) + f.setFlag(Qt::ItemIsEditable); //Suffix and TypeCol are deduced from Model + + return f; +} + +void RollingstockSQLModel::clearCache() +{ + cache.clear(); + cache.squeeze(); + cacheFirstRow = 0; +} + +void RollingstockSQLModel::refreshData() +{ + if(!mDb.db()) + return; + + emit itemsReady(-1, -1); //Notify we are about to refresh + + //TODO: consider filters + query q(mDb, "SELECT COUNT(1) FROM rs_list"); + 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 RollingstockSQLModel::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; + // 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 RollingstockSQLModel::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 rs_list.id,rs_list.number,rs_list.model_id,rs_list.owner_id," + "rs_models.name,rs_models.suffix,rs_models.type,rs_owners.name" + " FROM rs_list" + " LEFT JOIN rs_models ON rs_models.id=rs_list.model_id" + " LEFT JOIN rs_owners ON rs_owners.id=rs_list.owner_id"; + switch (sortCol) + { + case Model: + { + whereCol = "rs_models.name,rs_list.number"; //Order by 2 columns, no where clause + break; + } + case Owner: + { + whereCol = "rs_owners.name,rs_models.name,rs_list.number"; //Order by 3 columns, no where clause + break; + } + case TypeCol: + { + whereCol = "rs_models.type,rs_models.name,rs_list.number"; //Order by 3 columns, no where clause + break; + } + } + + if(val.isValid()) + { + sql += " WHERE "; + sql += whereCol; + if(reverse) + 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); + + if(val.isValid()) + { + switch (sortCol) + { + case Model: + { + 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); + item.number = r.get(1); + item.modelId = r.get(2); + item.ownerId = r.get(3); + item.modelName = r.get(4); + item.modelSuffix = r.get(5); + item.type = RsType(r.get(6)); + item.ownerName = r.get(7); + 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); + item.number = r.get(1); + item.modelId = r.get(2); + item.ownerId = r.get(3); + item.modelName = r.get(4); + item.modelSuffix = r.get(5); + item.type = RsType(r.get(6)); + item.ownerName = r.get(7); + i++; + } + if(i < BatchSize) + vec.remove(i, BatchSize - i); + } + + + RollingstockModelResultEvent *ev = new RollingstockModelResultEvent; + ev->items = vec; + ev->firstRow = first; + + qApp->postEvent(this, ev); +} + +void RollingstockSQLModel::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 itemsReady(firstRow, lastRow); + + qDebug() << "TOTAL: From:" << cacheFirstRow << "To:" << cacheFirstRow + cache.size() - 1; +} + +void RollingstockSQLModel::setSortingColumn(int col) +{ + if(sortColumn == col || col == Number || col == Suffix || col >= NCols) + return; + + clearCache(); + sortColumn = col; + + QModelIndex first = index(0, 0); + QModelIndex last = index(curItemCount - 1, NCols - 1); + emit dataChanged(first, last); +} + +bool RollingstockSQLModel::getFieldData(int row, int col, db_id &idOut, QString &nameOut) const +{ + if(row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; + + const RSItem& item = cache[row - cacheFirstRow]; + + switch (col) + { + case Model: + { + idOut = item.modelId; + nameOut = item.modelName; + break; + } + case Owner: + { + idOut = item.ownerId; + nameOut = item.ownerName; + break; + } + default: + return false; + } + + return true; +} + +bool RollingstockSQLModel::validateData(int /*row*/, int /*col*/, db_id /*id*/, const QString &/*name*/) +{ + return true; +} + +bool RollingstockSQLModel::setFieldData(int row, int col, db_id id, const QString &name) +{ + if(row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; + + RSItem& item = cache[row - cacheFirstRow]; + bool ret = false; + switch (col) + { + case Model: + { + ret = setModel(item, id, name); + break; + } + case Owner: + { + ret = setOwner(item, id, name); + break; + } + default: + break; + } + + if(ret) + { + QModelIndex idx = index(row, col); + emit dataChanged(idx, idx); + } + + return ret; +} + +bool RollingstockSQLModel::setModel(RSItem &item, db_id modelId, const QString &name) +{ + if(item.modelId == modelId) + return false; + + //FIXME: should be already handled by UNIQUE constraints + //Check if there is already that combination of + //Model.Number, if so set an higher number + query q_hasModelNumCombination(mDb, "SELECT id, model_id, number FROM rs_list WHERE model_id=? AND number=?"); + q_hasModelNumCombination.bind(1, modelId); + q_hasModelNumCombination.bind(2, item.number); + int ret = q_hasModelNumCombination.step(); + q_hasModelNumCombination.reset(); + + if(ret == SQLITE_ROW) + { + //There's already that Model.Number, change our number + query q_getMaxNumberOfThatModel(mDb, "SELECT MAX(number) FROM rs_list WHERE model_id=?"); + q_getMaxNumberOfThatModel.bind(1, modelId); + q_getMaxNumberOfThatModel.step(); + auto r = q_getMaxNumberOfThatModel.getRows(); + int number = r.get(0) + 1; //Max + 1 + q_getMaxNumberOfThatModel.reset(); + + //BIG TODO: numeri carri/carrozze hanno cifra di controllo, non posono essere aumentati di +1 a caso + //ERRORMSG: ask user + + command q_setNumber(mDb, "UPDATE rs_list SET number=? WHERE id=?"); + q_setNumber.bind(1, number); + q_setNumber.bind(2, item.rsId); + ret = q_setNumber.execute(); + q_setNumber.reset(); + + if(ret != SQLITE_OK) + return false; + + item.number = number; + } + + command q_setModel(mDb, "UPDATE rs_list SET model_id=? WHERE id=?"); + q_setModel.bind(1, modelId); + q_setModel.bind(2, item.rsId); + ret = q_setModel.execute(); + q_setModel.reset(); + + if(ret != SQLITE_OK) + return false; + + item.modelId = modelId; + Q_UNUSED(name) //We clear the cache so the name will be re-queried + + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + + emit Session->rollingStockModified(item.rsId); + + return true; +} + +bool RollingstockSQLModel::setOwner(RSItem &item, db_id ownerId, const QString& name) +{ + if(item.ownerId == ownerId) + return false; + + command q_setOwner(mDb, "UPDATE rs_list SET owner_id=? WHERE id=?"); + q_setOwner.bind(1, ownerId); + q_setOwner.bind(2, item.rsId); + int ret = q_setOwner.execute(); + q_setOwner.reset(); + + if(ret != SQLITE_OK) + return false; + + item.ownerId = ownerId; + item.ownerName = name; + + emit Session->rollingStockModified(item.rsId); + + if(sortColumn == Owner) + { + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + } + + return true; +} + +bool RollingstockSQLModel::setNumber(RSItem &item, int number) +{ + if(item.number == number) + return false; + + //Check if there is already that combination of + //Model.Number, if so don't set new number + //ERRORMSG: show error to user (emit error(int code, QString msg)) + //TODO: use UNIQUE constraint??? + command q_hasModelNumCombination(mDb, "SELECT id,model_id,number FROM rs_list WHERE model_id=? AND number=?"); + q_hasModelNumCombination.bind(1, item.modelId); + q_hasModelNumCombination.bind(2, number); + int ret = q_hasModelNumCombination.step(); + q_hasModelNumCombination.reset(); + + if(ret == SQLITE_ROW) //We already have one + return false; + + command q_setNumber(mDb, "UPDATE rs_list SET number=? WHERE id=?"); + q_setNumber.bind(1, number); + q_setNumber.bind(2, item.rsId); + ret = q_setNumber.execute(); + q_setNumber.reset(); + + if(ret != SQLITE_OK) + return false; + + item.number = number; + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + + emit Session->rollingStockModified(item.rsId); + + return true; +} + +bool RollingstockSQLModel::removeRSItem(db_id rsId, const RSItem *item) +{ + if(!rsId) + return false; + + command cmd(mDb, "DELETE FROM rs_list WHERE id=?"); + cmd.bind(1, rsId); + int ret = cmd.execute(); + if(ret != SQLITE_OK) + { + ret = mDb.extended_error_code(); + if(ret == SQLITE_CONSTRAINT_TRIGGER) + { + QString name; + if(item) + { + name = rs_utils::formatName(item->modelName, item->number, item->modelSuffix, item->type); + } + else + { + query q(mDb, "SELECT rs_list.number,rs_models.name,rs_models.suffix,rs_models.type" + " FROM rs_list" + " LEFT JOIN rs_models ON rs_models.id=rs_list.model_id" + " WHERE rs_list.id=?"); + q.bind(1, rsId); + q.step(); + + sqlite3_stmt *stmt = q.stmt(); + + int number = sqlite3_column_int(stmt, 0); + int modelNameLen = sqlite3_column_bytes(stmt, 1); + const char *modelName = reinterpret_cast(sqlite3_column_text(stmt, 1)); + + int modelSuffixLen = sqlite3_column_bytes(stmt, 2); + const char *modelSuffix = reinterpret_cast(sqlite3_column_text(stmt, 2)); + + RsType type = RsType(sqlite3_column_int(stmt, 3)); + + name = rs_utils::formatNameRef(modelName, modelNameLen, + number, + modelSuffix, modelSuffixLen, + type); + } + + emit modelError(tr(errorRSInUseCannotDelete).arg(name)); + return false; + } + qWarning() << "RollingstockSQLModel: error removing model" << ret << mDb.error_msg(); + return false; + } + + emit Session->rollingstockRemoved(rsId); + + refreshData(); + return true; +} + +bool RollingstockSQLModel::removeRSItemAt(int row) +{ + if(row >= curItemCount || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + const RSItem &item = cache.at(row - cacheFirstRow); + return removeRSItem(item.rsId, &item); +} + +db_id RollingstockSQLModel::addRSItem(int *outRow, QString *errOut) +{ + db_id rsId = 0; + + command cmd(mDb, "INSERT INTO rs_list(id,model_id,number,owner_id) VALUES (NULL,NULL,0,NULL)"); + sqlite3_mutex *mutex = sqlite3_db_mutex(mDb.db()); + sqlite3_mutex_enter(mutex); + int ret = cmd.execute(); + if(ret == SQLITE_OK) + { + rsId = mDb.last_insert_rowid(); + } + sqlite3_mutex_leave(mutex); + + if(ret == SQLITE_CONSTRAINT_UNIQUE) + { + //There is already an RS with no model set, use that instead + query findEmpty(mDb, "SELECT id FROM rs_list WHERE model_id=0 OR model_id IS NULL LIMIT 1"); + if(findEmpty.step() == SQLITE_ROW) + { + rsId = findEmpty.getRows().get(0); + } + } + else if(ret != SQLITE_OK) + { + if(errOut) + *errOut = mDb.error_msg(); + qDebug() << "RollingstockSQLModel Error adding:" << ret << mDb.error_msg() << mDb.error_code() << mDb.extended_error_code(); + } + + refreshData(); //Recalc row count + switchToPage(0); //Reset to first page and so it is shown as first row + + if(outRow) + *outRow = rsId ? 0 : -1; //Empty model is always the first + + return rsId; +} + +bool RollingstockSQLModel::removeAllRS() +{ + command cmd(mDb, "DELETE FROM rs_list"); + int ret = cmd.execute(); + if(ret != SQLITE_OK) + { + qWarning() << "Removing ALL RS:" << ret << mDb.extended_error_code() << "Err:" << mDb.error_msg(); + return false; + } + + refreshData(); + return true; +} diff --git a/src/rollingstock/rollingstocksqlmodel.h b/src/rollingstock/rollingstocksqlmodel.h new file mode 100644 index 0000000..b9b32c5 --- /dev/null +++ b/src/rollingstock/rollingstocksqlmodel.h @@ -0,0 +1,107 @@ +#ifndef ROLLINGSTOCKSQLMODEL_H +#define ROLLINGSTOCKSQLMODEL_H + +#include "utils/sqldelegate/pageditemmodel.h" +#include "utils/sqldelegate/IFKField.h" + +#include "utils/types.h" + +#include + +class RollingstockSQLModel : public IPagedItemModel, public IFKField +{ + Q_OBJECT + +public: + + enum { BatchSize = 100 }; + + typedef enum { + Model = 0, + Number, + Suffix, + Owner, + TypeCol, + NCols + } Columns; + + typedef struct RSItem_ + { + db_id rsId; + db_id modelId; + db_id ownerId; + QString modelName; + QString modelSuffix; + QString ownerName; + int number; + RsType type; + } RSItem; + + RollingstockSQLModel(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; + + // Editable: + bool setData(const QModelIndex &idx, const QVariant &value, + int role = Qt::EditRole) override; + + Qt::ItemFlags flags(const QModelIndex& idx) 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; + + // IFKField + bool getFieldData(int row, int col, db_id &idOut, QString &nameOut) const override; + bool validateData(int row, int col, db_id id, const QString &name) override; + bool setFieldData(int row, int col, db_id id, const QString &name) override; + + // RollingstockSQLModel + bool removeRSItem(db_id rsId, const RSItem *item = nullptr); + bool removeRSItemAt(int row); + db_id addRSItem(int *outRow, QString *errOut = nullptr); + + // Convinience + inline db_id getIdAtRow(int row) const + { + if (row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return 0; //Invalid + + const RSItem& item = cache.at(row - cacheFirstRow); + return item.rsId; + } + + bool removeAllRS(); + +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); + + bool setModel(RSItem &item, db_id modelId, const QString &name); + bool setOwner(RSItem &item, db_id ownerId, const QString &name); + bool setNumber(RSItem &item, int number); + +private: + QVector cache; + int cacheFirstRow; + int firstPendingRow; +}; + +#endif // ROLLINGSTOCKSQLMODEL_H diff --git a/src/rollingstock/rs_checker/CMakeLists.txt b/src/rollingstock/rs_checker/CMakeLists.txt new file mode 100644 index 0000000..69b02ef --- /dev/null +++ b/src/rollingstock/rs_checker/CMakeLists.txt @@ -0,0 +1,14 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + rollingstock/rs_checker/error_data.h + rollingstock/rs_checker/rscheckermanager.h + rollingstock/rs_checker/rserrorswidget.h + rollingstock/rs_checker/rserrortreemodel.h + rollingstock/rs_checker/rsworker.h + + rollingstock/rs_checker/rscheckermanager.cpp + rollingstock/rs_checker/rserrorswidget.cpp + rollingstock/rs_checker/rserrortreemodel.cpp + rollingstock/rs_checker/rsworker.cpp + PARENT_SCOPE +) diff --git a/src/rollingstock/rs_checker/error_data.h b/src/rollingstock/rs_checker/error_data.h new file mode 100644 index 0000000..8747cea --- /dev/null +++ b/src/rollingstock/rs_checker/error_data.h @@ -0,0 +1,50 @@ +#ifndef ERROR_DATA_H +#define ERROR_DATA_H + +#ifdef ENABLE_RS_CHECKER + +#include "utils/types.h" + +#include +#include + +namespace RsErrors +{ + +enum ErrType : quint32 +{ + NoError = 0, + StopIsTransit, + CoupledWhileBusy, //otherId: previous coupling id + UncoupledWhenNotCoupled, //otherId: previous uncoupling id + NotUncoupledAtJobEnd, //otherId: next operation id or zero + CoupledInDifferentStation, //otherId: previous coupling id + UncoupledInSameStop //otherId: previous coupling id +}; + +typedef struct ErrorData_ +{ + db_id couplingId; + db_id rsId; + db_id stopId; + db_id stationId; + db_id jobId; + db_id otherId; + QString stationName; + QTime time; + JobCategory jobCategory; + ErrType errorType; +} ErrorData; + +typedef struct RSErrorList_ +{ + db_id rsId; + QString rsName; + QVector errors; +} RSErrorList; + +} //namespace RsErrors + +#endif // ENABLE_RS_CHECKER + +#endif // ERROR_DATA_H diff --git a/src/rollingstock/rs_checker/rscheckermanager.cpp b/src/rollingstock/rs_checker/rscheckermanager.cpp new file mode 100644 index 0000000..3f3b74d --- /dev/null +++ b/src/rollingstock/rs_checker/rscheckermanager.cpp @@ -0,0 +1,149 @@ +#include "rscheckermanager.h" + +#ifdef ENABLE_RS_CHECKER + +#include + +#include "app/session.h" + +#include "rsworker.h" +#include "rserrortreemodel.h" + +RsCheckerManager::RsCheckerManager(QObject *parent) : + QObject(parent), + m_mainWorker(nullptr) +{ + errorsModel = new RsErrorTreeModel(this); +} + +RsCheckerManager::~RsCheckerManager() +{ + if(m_mainWorker) + { + m_mainWorker->stop(); + m_mainWorker->cleanup(); + m_mainWorker = nullptr; + } + + for(RsErrWorker *task : m_workers) + { + task->stop(); + task->cleanup(); + } + m_workers.clear(); +} + +bool RsCheckerManager::event(QEvent *e) +{ + if(e->type() == RsWorkerProgressEvent::_Type) + { + e->setAccepted(true); + + RsWorkerProgressEvent *ev = static_cast(e); + + if(ev->progress == 0) + { + emit progressMax(ev->progressMax); + } + + emit progress(ev->progress); + + return true; + } + else if(e->type() == RsWorkerResultEvent::_Type) + { + e->setAccepted(true); + + RsWorkerResultEvent *ev = static_cast(e); + + if(m_mainWorker && ev->task == m_mainWorker) + { + if(!m_mainWorker->wasStopped()) + { + errorsModel->setErrors(ev->results); + } + + delete m_mainWorker; + m_mainWorker = nullptr; + + emit taskFinished(); + } + else + { + int idx = m_workers.indexOf(ev->task); + if(idx != -1) + { + m_workers.removeAt(idx); + if(!ev->task->wasStopped()) + errorsModel->mergeErrors(ev->results); + + delete ev->task; + } + } + + return true; + } + + return QObject::event(e); +} + +bool RsCheckerManager::startWorker() +{ + if(m_mainWorker) + return false; + + if(!Session->m_Db.db()) + return false; + + m_mainWorker = new RsErrWorker(Session->m_Db, this, {}); + + QThreadPool::globalInstance()->start(m_mainWorker); + + for(RsErrWorker *task : m_workers) + { + if(!QThreadPool::globalInstance()->tryTake(task)) + task->stop(); + } + + return true; +} + +void RsCheckerManager::abortTasks() +{ + if(m_mainWorker) + { + m_mainWorker->stop(); + } + + for(RsErrWorker *task : m_workers) + { + task->stop(); + } +} + +void RsCheckerManager::checkRs(QSet set) +{ + if(set.isEmpty() || !Session->m_Db.db()) + return; + + QVector vec(set.size()); + for(db_id rsId : set) + vec.append(rsId); + + RsErrWorker *task = new RsErrWorker(Session->m_Db, this, vec); + m_workers.append(task); + + QThreadPool::globalInstance()->start(task); +} + +RsErrorTreeModel *RsCheckerManager::getErrorsModel() const +{ + return errorsModel; +} + +void RsCheckerManager::clearModel() +{ + errorsModel->clear(); +} + +#endif // ENABLE_RS_CHECKER diff --git a/src/rollingstock/rs_checker/rscheckermanager.h b/src/rollingstock/rs_checker/rscheckermanager.h new file mode 100644 index 0000000..662f773 --- /dev/null +++ b/src/rollingstock/rs_checker/rscheckermanager.h @@ -0,0 +1,49 @@ +#ifndef RSCHECKERMANAGER_H +#define RSCHECKERMANAGER_H + +#ifdef ENABLE_RS_CHECKER + +#include + +#include +#include + +#include "utils/types.h" + +class RsErrWorker; +class RsErrorTreeModel; + +class RsCheckerManager : public QObject +{ + Q_OBJECT +public: + explicit RsCheckerManager(QObject *parent = nullptr); + ~RsCheckerManager(); + + bool event(QEvent *e) override; + + bool startWorker(); + void abortTasks(); + inline bool isRunning() { return m_mainWorker || m_workers.size() > 0; } + + void checkRs(QSet set); + + RsErrorTreeModel *getErrorsModel() const; + + void clearModel(); + +signals: + void progressMax(int max); + void progress(int val); + void taskFinished(); + +private: + RsErrWorker *m_mainWorker; //Checks all rollingstock + QVector m_workers; //Specific check for some RS + + RsErrorTreeModel *errorsModel; +}; + +#endif // ENABLE_RS_CHECKER + +#endif // RSCHECKERMANAGER_H diff --git a/src/rollingstock/rs_checker/rserrorswidget.cpp b/src/rollingstock/rs_checker/rserrorswidget.cpp new file mode 100644 index 0000000..a293ceb --- /dev/null +++ b/src/rollingstock/rs_checker/rserrorswidget.cpp @@ -0,0 +1,141 @@ +#ifdef ENABLE_RS_CHECKER + +#include "rserrorswidget.h" + +#include +#include +#include +#include + +#include + +#include "app/session.h" + +#include "rserrortreemodel.h" + +#include "backgroundmanager/backgroundmanager.h" +#include "rscheckermanager.h" + +#include +#include + +#include "viewmanager/viewmanager.h" + + +RsErrorsWidget::RsErrorsWidget(QWidget *parent) : + QWidget(parent), + timerId(0) +{ + view = new QTreeView; + view->setUniformRowHeights(true); + view->setSelectionBehavior(QTreeView::SelectRows); + + progressBar = new QProgressBar; + startBut = new QPushButton(tr("Start")); + stopBut = new QPushButton(tr("Stop")); + + startBut->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + stopBut->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + + auto mgr = Session->getBackgroundManager()->getRsChecker(); + + view->setModel(mgr->getErrorsModel()); + view->header()->setStretchLastSection(true); + view->setSelectionBehavior(QTreeView::SelectRows); + + QGridLayout *grid = new QGridLayout(this); + grid->addWidget(view, 0, 0, 1, 3); + grid->addWidget(startBut, 1, 0, 1, 1); + grid->addWidget(stopBut, 1, 1, 1, 1); + grid->addWidget(progressBar, 1, 2, 1, 1); + + connect(mgr, &RsCheckerManager::progress, progressBar, &QProgressBar::setValue); + connect(mgr, &RsCheckerManager::progressMax, this, &RsErrorsWidget::taskProgressMax); + connect(mgr, &RsCheckerManager::taskFinished, this, &RsErrorsWidget::taskFinished); + + connect(startBut, &QPushButton::clicked, this, &RsErrorsWidget::startTask); + connect(stopBut, &QPushButton::clicked, this, &RsErrorsWidget::stopTask); + + view->setContextMenuPolicy(Qt::CustomContextMenu); + connect(view, &QTreeView::customContextMenuRequested, this, &RsErrorsWidget::showContextMenu); + + setWindowTitle(tr("Rollingstock Errors")); + + progressBar->hide(); +} + +void RsErrorsWidget::startTask() +{ + progressBar->setValue(0); + + if(Session->getBackgroundManager()->getRsChecker()->startWorker()) + { + if(timerId) + { + killTimer(timerId); //Stop progressBar from hiding in 1 second + timerId = 0; + } + } +} + +void RsErrorsWidget::stopTask() +{ + Session->getBackgroundManager()->abortAllTasks(); +} + +void RsErrorsWidget::taskProgressMax(int max) +{ + progressBar->setMaximum(max); + progressBar->show(); +} + +void RsErrorsWidget::taskFinished() +{ + progressBar->setValue(progressBar->maximum()); + + if(timerId) + killTimer(timerId); + timerId = startTimer(1000); //Hide progressBar after 1 second +} + +void RsErrorsWidget::timerEvent(QTimerEvent *event) +{ + if(event->timerId() == timerId) + { + killTimer(timerId); + timerId = 0; + progressBar->hide(); + } +} + +void RsErrorsWidget::showContextMenu(const QPoint& pos) +{ + QModelIndex idx = view->indexAt(pos); + if(!idx.isValid()) + return; + + const RsErrorTreeModel *model = static_cast(view->model()); + auto item = model->getItem(idx); + if(!item) + return; + + QMenu menu(this); + + QAction *showInJobEditor = new QAction(tr("Show in Job Editor"), &menu); + QAction *showRsPlan = new QAction(tr("Show rollingstock plan"), &menu); + + menu.addAction(showInJobEditor); + menu.addAction(showRsPlan); + + QAction *act = menu.exec(view->viewport()->mapToGlobal(pos)); + if(act == showInJobEditor) + { + Session->getViewManager()->requestJobEditor(item->jobId, item->stopId); + } + else if(act == showRsPlan) + { + Session->getViewManager()->requestRSInfo(item->rsId); + } +} + +#endif // ENABLE_RS_CHECKER diff --git a/src/rollingstock/rs_checker/rserrorswidget.h b/src/rollingstock/rs_checker/rserrorswidget.h new file mode 100644 index 0000000..e6a3054 --- /dev/null +++ b/src/rollingstock/rs_checker/rserrorswidget.h @@ -0,0 +1,38 @@ +#ifndef RSERRORSWIDGET_H +#define RSERRORSWIDGET_H + +#ifdef ENABLE_RS_CHECKER + +#include + +class QTreeView; +class QProgressBar; +class QPushButton; + +class RsErrorsWidget : public QWidget +{ + Q_OBJECT +public: + explicit RsErrorsWidget(QWidget *parent = nullptr); + +protected: + void timerEvent(QTimerEvent *event); + +private slots: + void startTask(); + void stopTask(); + void taskProgressMax(int max); + void taskFinished(); + void showContextMenu(const QPoint &pos); + +private: + QTreeView *view; + QProgressBar *progressBar; + QPushButton *startBut; + QPushButton *stopBut; + int timerId; +}; + +#endif // ENABLE_RS_CHECKER + +#endif // RSERRORSWIDGET_H diff --git a/src/rollingstock/rs_checker/rserrortreemodel.cpp b/src/rollingstock/rs_checker/rserrortreemodel.cpp new file mode 100644 index 0000000..32a3066 --- /dev/null +++ b/src/rollingstock/rs_checker/rserrortreemodel.cpp @@ -0,0 +1,262 @@ +#ifdef ENABLE_RS_CHECKER + +#include "rserrortreemodel.h" + +#include //For translations + +#include "utils/jobcategorystrings.h" + +static const char* error_texts[] = { + nullptr, + QT_TRANSLATE_NOOP("RsErrors", "Stop is transit. Cannot couple/uncouple rollingstock."), + QT_TRANSLATE_NOOP("RsErrors", "Coupled while busy: it was already coupled to another job."), + QT_TRANSLATE_NOOP("RsErrors", "Uncoupled when not coupled."), + QT_TRANSLATE_NOOP("RsErrors", "Not uncoupled at the end of the job or coupled by another job before this jobs uncouples it."), + QT_TRANSLATE_NOOP("RsErrors", "Coupled in a different station than that where it was uncoupled."), + QT_TRANSLATE_NOOP("RsErrors", "Uncoupled in the same stop it was coupled.") +}; + +class RsError +{ + Q_DECLARE_TR_FUNCTIONS(RsErrors) +}; + +RsErrorTreeModel::RsErrorTreeModel(QObject *parent) : + QAbstractItemModel(parent) +{ + //FIXME: listen for changes in rs/job/station names and update them +} + +QVariant RsErrorTreeModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch (section) + { + case JobName: + return tr("Job"); + case StationName: + return tr("Station"); + case Arrival: + return tr("Arrival"); + case Description: + return tr("Description"); + default: + break; + } + } + return QAbstractItemModel::headerData(section, orientation, role); +} + +QModelIndex RsErrorTreeModel::index(int row, int column, const QModelIndex &parent) const +{ + if(parent.isValid()) + { + if(parent.row() >= m_data.size() || parent.internalPointer()) + return QModelIndex(); //Out of bound or child-most + + auto it = m_data.constBegin() + parent.row(); + if(row >= it->errors.size()) + return QModelIndex(); + + void *ptr = const_cast(static_cast(&it->errors.at(row))); + return createIndex(row, column, ptr); + } + + if(row >= m_data.size()) + return QModelIndex(); + + return createIndex(row, column, nullptr); +} + +QModelIndex RsErrorTreeModel::parent(const QModelIndex &idx) const +{ + if(!idx.isValid()) + return QModelIndex(); + + RsErrors::ErrorData *item = static_cast(idx.internalPointer()); + if(!item) //Caption + return QModelIndex(); + + auto it = m_data.constFind(item->rsId); + if(it == m_data.constEnd()) + return QModelIndex(); + + int row = std::distance(m_data.constBegin(), it); + return index(row, 0); +} + +int RsErrorTreeModel::rowCount(const QModelIndex &parent) const +{ + if(parent.isValid()) + { + if(parent.internalPointer()) + return 0; //Child most + + auto it = m_data.constBegin() + parent.row(); + return it->errors.size(); + } + + return m_data.size(); +} + +int RsErrorTreeModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 1 : NCols; +} + +bool RsErrorTreeModel::hasChildren(const QModelIndex &parent) const +{ + if(parent.isValid()) + { + if(parent.internalPointer() || parent.row() >= m_data.size()) + return false; + + auto it = m_data.constBegin() + parent.row(); + return it->errors.size(); + } + + return m_data.size(); // size > 0 +} + +QModelIndex RsErrorTreeModel::sibling(int row, int column, const QModelIndex &idx) const +{ + if(!idx.isValid()) + return QModelIndex(); + + if(idx.internalPointer()) + { + return createIndex(row, column, idx.internalPointer()); + } + + if(column > 0 || row >= m_data.size()) + return QModelIndex(); + + return createIndex(row, 0, nullptr); +} + +QVariant RsErrorTreeModel::data(const QModelIndex &idx, int role) const +{ + if(!idx.isValid() || role != Qt::DisplayRole) + return QVariant(); + + RsErrors::ErrorData *item = static_cast(idx.internalPointer()); + if(item) + { + switch (idx.column()) + { + case JobName: + return JobCategoryName::jobName(item->jobId, item->jobCategory); + case StationName: + return item->stationName; + case Arrival: + return item->time; + case Description: + return RsError::tr(error_texts[item->errorType]); + } + } + else + { + //Caption + if(idx.row() >= m_data.size()) + return QVariant(); + + auto it = m_data.constBegin() + idx.row(); + if(idx.column() == 0) + return it->rsName; + } + + return QVariant(); +} + +/* Description: + * Clear current errors and set new ones +*/ +void RsErrorTreeModel::setErrors(const QMap &data) +{ + beginResetModel(); + m_data = data; + endResetModel(); +} + +/* Description: + * Merge new errors with pre-existing. + * - If an RS is passed with no errors (i.e. empty QVector) it gets removed from the model + * - If an new RS is passed it gets inserted in the model + * - If an RS already in the model is passed then its current errors are cleared and the new errors are inserted +*/ +void RsErrorTreeModel::mergeErrors(const QMap &data) +{ + Data::iterator oldIter = m_data.begin(); + int row = 0; + for(auto it = data.constBegin(); it != data.constEnd(); it++) + { + auto iter = m_data.find(it.key()); + if(iter == m_data.end()) //Insert a new RS + { + if(it->errors.isEmpty()) + continue; //Error: tried to remove an RS not in this model (maybe already removed) + + Data::iterator pos = m_data.lowerBound(it.key()); + row += std::distance(oldIter, pos); + + beginInsertRows(QModelIndex(), row, row); + iter = m_data.insert(pos, it.key(), it.value()); + endInsertRows(); + } + else + { + row += std::distance(oldIter, iter); + + if(it->errors.isEmpty()) //Remove RS + { + beginRemoveRows(QModelIndex(), row, row); + iter = m_data.erase(iter); + endRemoveRows(); + } + else //Repopulate + { + //First remove old rows to invalidate QModelIndex because they store a pointer to vector elements that will become dangling + QModelIndex parent = createIndex(row, 0, nullptr); + beginRemoveRows(parent, 0, iter->errors.size() - 1); + iter->errors.clear(); + endRemoveRows(); + + beginInsertRows(parent, 0, it->errors.size() - 1); + iter.value() = it.value(); //Copy errors QVector + endInsertRows(); + } + } + + oldIter = iter; + } +} + +void RsErrorTreeModel::clear() +{ + beginResetModel(); + m_data.clear(); + endResetModel(); +} + +RsErrors::ErrorData* RsErrorTreeModel::getItem(const QModelIndex& idx) const +{ + if(idx.internalPointer()) + return static_cast(idx.internalPointer()); + return nullptr; +} + +void RsErrorTreeModel::onRSInfoChanged(db_id rsId) +{ + Q_UNUSED(rsId) + //Update top-level items that show RS names + if(m_data.size()) + { + QModelIndex first = index(0, 0); + QModelIndex last = index(m_data.size(), 0); + + emit dataChanged(first, last); + } +} + +#endif // ENABLE_RS_CHECKER diff --git a/src/rollingstock/rs_checker/rserrortreemodel.h b/src/rollingstock/rs_checker/rserrortreemodel.h new file mode 100644 index 0000000..e0e7f8f --- /dev/null +++ b/src/rollingstock/rs_checker/rserrortreemodel.h @@ -0,0 +1,62 @@ +#ifndef RSERRORTREEMODEL_H +#define RSERRORTREEMODEL_H + +#ifdef ENABLE_RS_CHECKER + +#include + +#include "error_data.h" + +//TODO: make on-demand +class RsErrorTreeModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + typedef QMap Data; + + enum Columns + { + JobName = 0, + StationName, + Arrival, + Description, + NCols + }; + + RsErrorTreeModel(QObject *parent = nullptr); + + // Header: + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + // Basic functionality: + QModelIndex index(int row, int column, + const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &idx) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; + + QModelIndex sibling(int row, int column, const QModelIndex &idx) const override; + + QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override; + + void setErrors(const QMap &data); + + void mergeErrors(const QMap &data); + + void clear(); + + RsErrors::ErrorData *getItem(const QModelIndex &idx) const; + +private slots: + void onRSInfoChanged(db_id rsId); + +private: + Data m_data; +}; + +#endif // ENABLE_RS_CHECKER + +#endif // RSERRORTREEMODEL_H diff --git a/src/rollingstock/rs_checker/rsworker.cpp b/src/rollingstock/rs_checker/rsworker.cpp new file mode 100644 index 0000000..521d0a8 --- /dev/null +++ b/src/rollingstock/rs_checker/rsworker.cpp @@ -0,0 +1,279 @@ +#ifdef ENABLE_RS_CHECKER + +#include "rsworker.h" + +#include "utils/types.h" +#include "utils/rs_utils.h" + +#include + +#include "error_data.h" + +#include + +#include +using namespace sqlite3pp; + +RsErrWorker::RsErrWorker(database &db, QObject *receiver, QVector vec) : + IQuittableTask(receiver), + mDb(db), + rsToCheck(vec) +{ +} + +void RsErrWorker::run() +{ + using namespace RsErrors; + + + query q_selectCoupling(mDb, "SELECT coupling.id, coupling.operation, coupling.stopId," + " stops.jobId, jobs.category," + " stops.stationId, stations.name," + " stops.transit, stops.arrival, stops.departure" + " FROM coupling" + " JOIN stops ON stops.id=coupling.stopId" + " JOIN jobs ON jobs.id=stops.jobId" + " JOIN stations ON stations.id=stops.stationId" + " WHERE coupling.rsId=? ORDER BY stops.arrival ASC"); + + qDebug() << "Starting WORKER: rs check"; + + QMap data; + + if(rsToCheck.isEmpty()) + { + query q_countRs(mDb, "SELECT COUNT() FROM rs_list"); + query q_selectRs(mDb, "SELECT rs_list.id,rs_list.number," + "rs_models.name,rs_models.suffix,rs_models.type" + " FROM rs_list" + " LEFT JOIN rs_models ON rs_models.id=rs_list.model_id"); + + q_countRs.step(); + int rsCount = q_countRs.getRows().get(0); + q_countRs.reset(); + + int i = 0; + sendEvent(new RsWorkerProgressEvent(0, rsCount), false); + + for(auto r : q_selectRs) + { + if(++i % 4 == 0) //Check every 4 RS to keep overhead low. + { + if(wasStopped()) + break; + + sendEvent(new RsWorkerProgressEvent(i, rsCount), false); + } + + RSErrorList rs; + rs.rsId = r.get(0); + + int number = r.get(1); + int modelNameLen = sqlite3_column_bytes(q_selectRs.stmt(), 2); + const char *modelName = reinterpret_cast(sqlite3_column_text(q_selectRs.stmt(), 2)); + + int modelSuffixLen = sqlite3_column_bytes(q_selectRs.stmt(), 3); + const char *modelSuffix = reinterpret_cast(sqlite3_column_text(q_selectRs.stmt(), 3)); + RsType type = RsType(r.get(4)); + + rs.rsName = rs_utils::formatNameRef(modelName, modelNameLen, + number, + modelSuffix, modelSuffixLen, + type); + + checkRs(rs, q_selectCoupling); + + if(rs.errors.size()) //Insert only if there are errors + data.insert(rs.rsId, rs); + } + } + else + { + query q_getRsInfo(mDb, "SELECT rs_list.number," + "rs_models.name,rs_models.suffix,rs_models.type" + " FROM rs_list" + " LEFT JOIN rs_models ON rs_models.id=rs_list.model_id" + " WHERE rs_list.id=?"); + int i = 0; + for(db_id rsId : rsToCheck) + { + if(++i % 4 == 0 && wasStopped()) //Check every 4 RS to keep overhead low. + break; + + RSErrorList rs; + rs.rsId = rsId; + + q_getRsInfo.bind(1, rs.rsId); + if(q_getRsInfo.step() != SQLITE_ROW) + { + q_getRsInfo.reset(); + continue; //RS does not exist! + } + + int number = sqlite3_column_int(q_getRsInfo.stmt(), 0); + int modelNameLen = sqlite3_column_bytes(q_getRsInfo.stmt(), 1); + const char *modelName = reinterpret_cast(sqlite3_column_text(q_getRsInfo.stmt(), 1)); + + int modelSuffixLen = sqlite3_column_bytes(q_getRsInfo.stmt(), 2); + const char *modelSuffix = reinterpret_cast(sqlite3_column_text(q_getRsInfo.stmt(), 2)); + RsType type = RsType(sqlite3_column_int(q_getRsInfo.stmt(), 3)); + + rs.rsName = rs_utils::formatNameRef(modelName, modelNameLen, + number, + modelSuffix, modelSuffixLen, + type); + q_getRsInfo.reset(); + + checkRs(rs, q_selectCoupling); + + //Insert also if there aren't errors to tell RsErrorTreeModel to remove this RS + data.insert(rs.rsId, rs); + } + } + + sendEvent(new RsWorkerResultEvent(this, data, !rsToCheck.isEmpty()), true); +} + +void RsErrWorker::checkRs(RsErrors::RSErrorList &rs, query& q_selectCoupling) +{ + using namespace RsErrors; + ErrorData err; + err.rsId = rs.rsId; + + RsOp prevOp = RsOp::Uncoupled; + + db_id prevCouplingId = 0; + db_id prevStopId = 0; + db_id prevStation = 0; + db_id prevJobId = 0; + QTime prevTime; + + q_selectCoupling.bind(1, err.rsId); + for(auto coup : q_selectCoupling) + { + err.couplingId = coup.get(0); + RsOp op = RsOp(coup.get(1)); + err.stopId = coup.get(2); + err.jobId = coup.get(3); + err.jobCategory = JobCategory(coup.get(4)); + err.stationId = coup.get(5); + err.stationName = coup.get(6); + int transit = coup.get(7); + QTime arrival = coup.get(8); + //QTime departure = coup.get(9); TODO: check departure less than next arrival + + err.time = arrival; //TODO: maybe arrival or departure depending + + err.otherId = prevCouplingId; + + if(op == prevOp) + { + if(op == RsOp::Coupled) + { + if(err.jobId != prevJobId && prevJobId != 0) + { + //Rs was not uncoupled at the end of the job + //Or it was coupled by another job before prevJob uncouples it + //NOTE: this might be a false positive. Example below: + // 00:00 - Job 1 couples Rs + // 00:30 - Job 2 couples Rs + // --> here we detect 'Coupled twice' and 'Not uncoupled at end of job' + // 00:45 - Job 2 uncouples Rs + // 00:50 - Job 1 uncouples Rs + // --> here we detect 'Uncoupled when not coupled' + // because Job 2 already has uncoupled. + // But this also means that it's not true that Rs isn't uncoupled at end of the jobs + + + //Here we create another structure to fill it with previous data + ErrorData e; + e.couplingId = prevCouplingId; + e.rsId = rs.rsId; + e.stopId = prevStopId; + e.stationId = prevStation; + e.jobId = prevJobId; + e.otherId = e.couplingId; + e.time = prevTime; + e.errorType = NotUncoupledAtJobEnd; + rs.errors.append(e); + } + + //Inform Rs was also coupled twice + err.errorType = CoupledWhileBusy; + + } + else + { + err.errorType = UncoupledWhenNotCoupled; + } + rs.errors.append(err); + } + + if(transit) + { + err.errorType = StopIsTransit; + rs.errors.append(err); + } + + if(op == Coupled && prevOp == Uncoupled && err.stationId != prevStation && prevStation != 0) + { + err.errorType = CoupledInDifferentStation; + rs.errors.append(err); + } + + if(op == Uncoupled && prevOp == Coupled && err.jobId != prevJobId && prevJobId != 0) + { + err.errorType = UncoupledWhenNotCoupled; + rs.errors.append(err); + } + + if(err.stopId == prevStopId) + { + err.errorType = UncoupledInSameStop; + rs.errors.append(err); + } + + prevOp = op; + prevCouplingId = err.couplingId; + prevStopId = err.stopId; + prevStation = err.stationId; + prevJobId = err.jobId; + prevTime = err.time; + } + q_selectCoupling.reset(); + + if(prevOp == RsOp::Coupled) + { + err.errorType = NotUncoupledAtJobEnd; + rs.errors.append(err); + } +} + +RsWorkerProgressEvent::RsWorkerProgressEvent(int pr, int max) : + QEvent(_Type), + progress(pr), + progressMax(max) +{ + +} + +RsWorkerProgressEvent::~RsWorkerProgressEvent() +{ + +} + +RsWorkerResultEvent::RsWorkerResultEvent(RsErrWorker *worker, const QMap &data, bool merge) : + QEvent(_Type), + task(worker), + results(data), + mergeErrors(merge) +{ + +} + +RsWorkerResultEvent::~RsWorkerResultEvent() +{ + +} + +#endif // ENABLE_RS_CHECKER diff --git a/src/rollingstock/rs_checker/rsworker.h b/src/rollingstock/rs_checker/rsworker.h new file mode 100644 index 0000000..318fefc --- /dev/null +++ b/src/rollingstock/rs_checker/rsworker.h @@ -0,0 +1,70 @@ +#ifndef RSWORKER_H +#define RSWORKER_H + +#ifdef ENABLE_RS_CHECKER + +#ifndef ENABLE_BACKGROUND_MANAGER +#error "Cannot use ENABLE_RS_CHECKER without ENABLE_BACKGROUND_MANAGER" +#endif + +#include "utils/thread/iquittabletask.h" + +#include +#include + +#include "utils/worker_event_types.h" + +#include "error_data.h" + + +namespace sqlite3pp +{ +class database; +class query; +} + +class RsErrWorker : public IQuittableTask +{ +public: + RsErrWorker(sqlite3pp::database& db, QObject *receiver, QVector vec); + + void run() override; + +private: + void checkRs(RsErrors::RSErrorList &rs, sqlite3pp::query &q_selectCoupling); + void finish(const QMap &results, bool merge); + +private: + sqlite3pp::database &mDb; + + QVector rsToCheck; +}; + +class RsWorkerProgressEvent : public QEvent +{ +public: + static const Type _Type = Type(CustomEvents::RsErrWorkerProgress); + + RsWorkerProgressEvent(int pr, int max); + ~RsWorkerProgressEvent(); + + int progress; + int progressMax; +}; + +class RsWorkerResultEvent : public QEvent +{ +public: + static const Type _Type = Type(CustomEvents::RsErrWorkerResult); + + RsWorkerResultEvent(RsErrWorker *worker, const QMap &data, bool merge); + ~RsWorkerResultEvent(); + + RsErrWorker *task; + QMap results; + bool mergeErrors; +}; + +#endif // ENABLE_RS_CHECKER + +#endif // RSWORKER_H diff --git a/src/rollingstock/rsmatchmodelfactory.cpp b/src/rollingstock/rsmatchmodelfactory.cpp new file mode 100644 index 0000000..c5105e0 --- /dev/null +++ b/src/rollingstock/rsmatchmodelfactory.cpp @@ -0,0 +1,27 @@ +#include "rsmatchmodelfactory.h" + +#include "rsmodelsmatchmodel.h" +#include "rsownersmatchmodel.h" +#include "rollingstockmatchmodel.h" + +RSMatchModelFactory::RSMatchModelFactory(ModelModes::Mode mode, sqlite3pp::database &db, QObject *parent) : + IMatchModelFactory(parent), + mDb(db), + m_mode(mode) +{ + +} + +ISqlFKMatchModel *RSMatchModelFactory::createModel() +{ + switch (m_mode) + { + case ModelModes::Models: + return new RSModelsMatchModel(mDb); + case ModelModes::Owners: + return new RSOwnersMatchModel(mDb); + case ModelModes::Rollingstock: + return new RollingstockMatchModel(mDb); + } + return nullptr; +} diff --git a/src/rollingstock/rsmatchmodelfactory.h b/src/rollingstock/rsmatchmodelfactory.h new file mode 100644 index 0000000..cc0e09a --- /dev/null +++ b/src/rollingstock/rsmatchmodelfactory.h @@ -0,0 +1,25 @@ +#ifndef RSMATCHMODELFACTORY_H +#define RSMATCHMODELFACTORY_H + +#include "utils/sqldelegate/imatchmodelfactory.h" + +#include "utils/model_mode.h" + +namespace sqlite3pp +{ +class database; +} + +class RSMatchModelFactory : public IMatchModelFactory +{ +public: + RSMatchModelFactory(ModelModes::Mode mode, sqlite3pp::database &db, QObject *parent); + + ISqlFKMatchModel *createModel() override; + +private: + sqlite3pp::database &mDb; + ModelModes::Mode m_mode; +}; + +#endif // RSMATCHMODELFACTORY_H diff --git a/src/rollingstock/rsmodelsmatchmodel.cpp b/src/rollingstock/rsmodelsmatchmodel.cpp new file mode 100644 index 0000000..62b78ad --- /dev/null +++ b/src/rollingstock/rsmodelsmatchmodel.cpp @@ -0,0 +1,136 @@ +#include "rsmodelsmatchmodel.h" + +RSModelsMatchModel::RSModelsMatchModel(database &db, QObject *parent) : + ISqlFKMatchModel(parent), + mDb(db), + q_getMatches(mDb) +{ + q_getMatches.prepare("SELECT id,name,suffix FROM rs_models WHERE name LIKE ?1 OR suffix LIKE ?1 LIMIT " QT_STRINGIFY(MaxMatchItems + 1)); +} + +QVariant RSModelsMatchModel::data(const QModelIndex &idx, int role) const +{ + if (!idx.isValid() || idx.row() >= size) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + { + if(isEmptyRow(idx.row())) + { + return ISqlFKMatchModel::tr("Empty"); + } + else if(isEllipsesRow(idx.row())) + { + return ellipsesString; + } + + return items[idx.row()].nameWithSuffix; + } + case Qt::FontRole: + { + if(isEmptyRow(idx.row())) + { + return boldFont(); + } + break; + } + } + + return QVariant(); +} + +void RSModelsMatchModel::autoSuggest(const QString &text) +{ + mQuery.clear(); + if(!text.isEmpty()) + { + mQuery.clear(); + mQuery.reserve(text.size() + 2); + mQuery.append('%'); + mQuery.append(text.toUtf8()); + mQuery.append('%'); + } + + refreshData(); +} + +void RSModelsMatchModel::refreshData() +{ + if(!mDb.db()) + return; + + beginResetModel(); + + char emptyQuery = '%'; + + if(mQuery.isEmpty()) + sqlite3_bind_text(q_getMatches.stmt(), 1, &emptyQuery, 1, SQLITE_STATIC); + else + sqlite3_bind_text(q_getMatches.stmt(), 1, mQuery, mQuery.size(), SQLITE_STATIC); + + + auto end = q_getMatches.end(); + auto it = q_getMatches.begin(); + int i = 0; + for(; i < MaxMatchItems && it != end; i++) + { + items[i].modelId = (*it).get(0); + + items[i].nameLength = sqlite3_column_bytes(q_getMatches.stmt(), 1); + const char *name = reinterpret_cast(sqlite3_column_text(q_getMatches.stmt(), 1)); + + int suffixLength = sqlite3_column_bytes(q_getMatches.stmt(), 2); + const char *suffix = reinterpret_cast(sqlite3_column_text(q_getMatches.stmt(), 2)); + + if(suffix && suffixLength) + { + QByteArray buf; + buf.reserve(items[i].nameLength + suffixLength + 3); + buf.append(name, items[i].nameLength); + buf.append(" (", 2); + buf.append(suffix, suffixLength); + buf.append(')'); + items[i].nameWithSuffix = QString::fromUtf8(buf); + }else{ + items[i].nameWithSuffix = QString::fromUtf8(name, items[i].nameLength); + } + ++it; + } + + size = i + 1; //Items + Empty + + if(it != end) + { + //There would be still rows, show Ellipses + size++; //Items + Empty + Ellispses + } + + q_getMatches.reset(); + endResetModel(); + + emit resultsReady(false); +} + +QString RSModelsMatchModel::getName(db_id id) const +{ + if(!mDb.db()) + return QString(); + + query q(mDb, "SELECT name FROM rs_models WHERE id=?"); + q.bind(1, id); + if(q.step() == SQLITE_ROW) + return q.getRows().get(0); + return QString(); +} + +db_id RSModelsMatchModel::getIdAtRow(int row) const +{ + return items[row].modelId; +} + +QString RSModelsMatchModel::getNameAtRow(int row) const +{ + return items[row].nameWithSuffix.left(items[row].nameLength); //Remove the suffix, keep only the name +} diff --git a/src/rollingstock/rsmodelsmatchmodel.h b/src/rollingstock/rsmodelsmatchmodel.h new file mode 100644 index 0000000..ae13a21 --- /dev/null +++ b/src/rollingstock/rsmodelsmatchmodel.h @@ -0,0 +1,43 @@ +#ifndef RSMODELSMATCHMODEL_H +#define RSMODELSMATCHMODEL_H + +#include "utils/sqldelegate/isqlfkmatchmodel.h" + +#include "utils/types.h" + +#include +using namespace sqlite3pp; + +class RSModelsMatchModel : public ISqlFKMatchModel +{ + Q_OBJECT + +public: + RSModelsMatchModel(database &db, QObject *parent = nullptr); + + // QAbstractTableModel + QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override; + + // ISqlFKMatchModel + void autoSuggest(const QString& text) override; + virtual void refreshData() override; + QString getName(db_id id) const override; + + db_id getIdAtRow(int row) const override; + QString getNameAtRow(int row) const override; + +private: + struct RSModelItem + { + db_id modelId; + QString nameWithSuffix; + int nameLength; + }; + RSModelItem items[MaxMatchItems]; + + database &mDb; + query q_getMatches; + QByteArray mQuery; +}; + +#endif // RSMODELSMATCHMODEL_H diff --git a/src/rollingstock/rsmodelssqlmodel.cpp b/src/rollingstock/rsmodelssqlmodel.cpp new file mode 100644 index 0000000..06936bd --- /dev/null +++ b/src/rollingstock/rsmodelssqlmodel.cpp @@ -0,0 +1,791 @@ +#include "rsmodelssqlmodel.h" + +#include +#include + +#include +using namespace sqlite3pp; + +#include "utils/model_roles.h" +#include "utils/rs_types_names.h" +#include "utils/worker_event_types.h" + +#include + +class RSModelsResultEvent : public QEvent +{ +public: + static constexpr Type _Type = Type(CustomEvents::RsModelsModelResult); + inline RSModelsResultEvent() :QEvent(_Type) {} + + QVector items; + int firstRow; +}; + +static constexpr char +errorModelNameAlreadyUsedWithSameSuffix[] = QT_TRANSLATE_NOOP("RSModelsSQLModel", + "This model name (%1) is already used with the same" + " suffix (%2).\n" + "If you intend to create a new model of same name but different" + " suffix, please first set the suffix."); + +static constexpr char +errorModelSuffixAlreadyUsedWithSameName[] = QT_TRANSLATE_NOOP("RSModelsSQLModel", + "This model suffix (%1) is already used with the same" + " name (%2)."); + +static constexpr char +errorSpeedMustBeGreaterThanZero[] = QT_TRANSLATE_NOOP("RSModelsSQLModel", + "Rollingstock maximum speed must be > 0 km/h."); + +static constexpr char +errorAtLeastTwoAxes[] = QT_TRANSLATE_NOOP("RSModelsSQLModel", + "Rollingstock must have at least 2 axes."); + +static constexpr char +errorModelInUseCannotDelete[] = QT_TRANSLATE_NOOP("RSModelsSQLModel", + "There are rollingstock pieces of model %1 so it cannot be removed.\n" + "If you wish to remove it, please first delete all %1 pieces."); + +RSModelsSQLModel::RSModelsSQLModel(sqlite3pp::database &db, QObject *parent) : + IPagedItemModel(500, db, parent), + cacheFirstRow(0), + firstPendingRow(-BatchSize) +{ + sortColumn = Name; +} + +bool RSModelsSQLModel::event(QEvent *e) +{ + if(e->type() == RSModelsResultEvent::_Type) + { + RSModelsResultEvent *ev = static_cast(e); + ev->setAccepted(true); + + handleResult(ev->items, ev->firstRow); + + return true; + } + + return QAbstractTableModel::event(e); +} + +QVariant RSModelsSQLModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(role == Qt::DisplayRole) + { + if(orientation == Qt::Horizontal) + { + switch (section) + { + case Name: + return RsTypeNames::tr("Name"); + case Suffix: + return RsTypeNames::tr("Suffix"); + case MaxSpeed: + return RsTypeNames::tr("Max Speed"); + case Axes: + return RsTypeNames::tr("N. Axes"); + case TypeCol: + return RsTypeNames::tr("Type"); + case SubTypeCol: + return RsTypeNames::tr("Subtype"); + default: + break; + } + } + else + { + return section + curPage * ItemsPerPage + 1; + } + } + return IPagedItemModel::headerData(section, orientation, role); +} + +int RSModelsSQLModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : curItemCount; +} + +int RSModelsSQLModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant RSModelsSQLModel::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 RSModel& item = cache.at(row - cacheFirstRow); + + switch (role) + { + case Qt::DisplayRole: + { + switch (idx.column()) + { + case Name: + return item.name; + case Suffix: + return item.suffix; + case MaxSpeed: + return QStringLiteral("%1 km/h").arg(item.maxSpeedKmH); //TODO: maybe QString('%1 km/h').arg(maxSpeed) AND custom spinBox with suffix + case Axes: + return int(item.axes); + case TypeCol: + { + return RsTypeNames::name(item.type); + } + case SubTypeCol: + { + if(item.type != RsType::Engine) + break; + + return RsTypeNames::name(item.sub_type); + } + } + + break; + } + case Qt::EditRole: + { + switch (idx.column()) + { + case Name: + return item.name; + case Suffix: + return item.suffix; + case MaxSpeed: + return item.maxSpeedKmH; + case Axes: + return int(item.axes); + } + + break; + } + case Qt::TextAlignmentRole: + { + if(idx.column() == MaxSpeed || idx.column() == Axes) + return Qt::AlignRight + Qt::AlignVCenter; + break; + } + case RS_MODEL_ID: + return item.modelId; + case RS_TYPE_ROLE: + return int(item.type); + case RS_SUB_TYPE_ROLE: + return int(item.sub_type); + } + + return QVariant(); +} + +bool RSModelsSQLModel::setData(const QModelIndex &idx, const QVariant &value, int role) +{ + const int row = idx.row(); + if(!idx.isValid() || row >= curItemCount || idx.column() >= NCols || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + RSModel &item = cache[row - cacheFirstRow]; + QModelIndex first = idx; + QModelIndex last = idx; + + switch (role) + { + case Qt::EditRole: + { + switch (idx.column()) + { + case Name: + { + const QString newName = value.toString().simplified(); + if(!setNameOrSuffix(item, newName, false)) + return false; + break; + } + case Suffix: + { + const QString newName = value.toString().simplified(); + if(!setNameOrSuffix(item, newName, true)) + return false; + break; + } + case MaxSpeed: + { + int speedKmH = value.toInt(); + if(speedKmH < 1) + { + emit modelError(tr(errorSpeedMustBeGreaterThanZero)); + return false; + } + + if(item.maxSpeedKmH == speedKmH) + return false; //No change + + command set_speed(mDb, "UPDATE rs_models SET max_speed=? WHERE id=?"); + set_speed.bind(1, speedKmH); + set_speed.bind(2, item.modelId); + if(set_speed.execute() != SQLITE_OK) + return false; + + item.maxSpeedKmH = qint16(speedKmH); + break; + } + case Axes: + { + int axes = value.toInt(); + if(axes < 2) + { + emit modelError(tr(errorAtLeastTwoAxes)); + return false; + } + + if(item.axes == axes) + return false; //No change + + command set_axes(mDb, "UPDATE rs_models SET axes=? WHERE id=?"); + set_axes.bind(1, axes); + set_axes.bind(2, item.modelId); + if(set_axes.execute() != SQLITE_OK) + return false; + + item.axes = qint8(axes); + break; + } + default: + break; + } + break; + } + case RS_TYPE_ROLE: //Set through RS_TYPE_ROLE only + { + if(!setType(item, RsType(value.toInt()), RsEngineSubType::NTypes)) + return false; + first = index(row, TypeCol); + last = index(row, SubTypeCol); + break; + } + case RS_SUB_TYPE_ROLE: + { + if(!setType(item, RsType::NTypes, RsEngineSubType(value.toInt()))) + return false; + first = index(row, TypeCol); + last = index(row, SubTypeCol); + break; + } + default: + break; + } + + emit dataChanged(first, last); + return true; +} + +Qt::ItemFlags RSModelsSQLModel::flags(const QModelIndex &idx) const +{ + if (!idx.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren; + if(idx.row() < cacheFirstRow || idx.row() >= cacheFirstRow + cache.size()) + return f; //Not fetched yet + + if(idx.column() != SubTypeCol || cache[idx.row() - cacheFirstRow].type == RsType::Engine) + f.setFlag(Qt::ItemIsEditable); //NOTE: SubTypeCol is editable only fot Engines + + return f; +} + +void RSModelsSQLModel::clearCache() +{ + cache.clear(); + cache.squeeze(); + cacheFirstRow = 0; +} + +void RSModelsSQLModel::refreshData() +{ + if(!mDb.db()) + return; + + //TODO: consider filters + query q(mDb, "SELECT COUNT(1) FROM rs_models"); + 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 RSModelsSQLModel::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; + // RSModel *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 RSModelsSQLModel::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 id,name,suffix,max_speed,axes,type,sub_type FROM rs_models"; + switch (sortCol) + { + case Name: + { + whereCol = "name,suffix"; //Order by 2 columns, no where clause + break; + } + case TypeCol: + { + whereCol = "type,sub_type,name,suffix"; //Order by 4 columns, no where clause + break; + } + } + + if(val.isValid()) + { + sql += " WHERE "; + sql += whereCol; + if(reverse) + 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); + + 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; + RSModel &item = vec[i]; + item.modelId = r.get(0); + item.name = r.get(1); + item.suffix = r.get(2); + item.maxSpeedKmH = r.get(3); + item.axes = r.get(4); + item.type = RsType(r.get(5)); + item.sub_type = RsEngineSubType(r.get(6)); + i--; + } + if(i > -1) + vec.remove(0, i + 1); + } + else + { + int i = 0; + + for(; it != end; ++it) + { + auto r = *it; + RSModel &item = vec[i]; + item.modelId = r.get(0); + item.name = r.get(1); + item.suffix = r.get(2); + item.maxSpeedKmH = r.get(3); + item.axes = r.get(4); + item.type = RsType(r.get(5)); + item.sub_type = RsEngineSubType(r.get(6)); + i++; + } + if(i < BatchSize) + vec.remove(i, BatchSize - i); + } + + + RSModelsResultEvent *ev = new RSModelsResultEvent; + ev->items = vec; + ev->firstRow = first; + + qApp->postEvent(this, ev); +} + +void RSModelsSQLModel::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); + + qDebug() << "TOTAL: From:" << cacheFirstRow << "To:" << cacheFirstRow + cache.size() - 1; +} + +void RSModelsSQLModel::setSortingColumn(int col) +{ + if(sortColumn == col || (col != Name && col != TypeCol)) + return; + + clearCache(); + sortColumn = col; + + QModelIndex first = index(0, 0); + QModelIndex last = index(curItemCount - 1, NCols - 1); + emit dataChanged(first, last); +} + +bool RSModelsSQLModel::setNameOrSuffix(RSModel& item, const QString& newName, bool suffix) +{ + if(suffix ? item.suffix == newName : item.name == newName) + return false; //No change + + command set_name(mDb, suffix ? "UPDATE rs_models SET suffix=? WHERE id=?" + : "UPDATE rs_models SET name=? WHERE id=?"); + set_name.bind(1, newName); + set_name.bind(2, item.modelId); + int ret = set_name.execute(); + if(ret != SQLITE_OK) + { + qDebug() << "setNameOrSuffix()" << ret << mDb.error_code() << mDb.extended_error_code() << mDb.error_msg(); + ret = mDb.extended_error_code(); + if(ret == SQLITE_CONSTRAINT_UNIQUE) + { + emit modelError(tr(suffix ? errorModelSuffixAlreadyUsedWithSameName + : errorModelNameAlreadyUsedWithSameSuffix) + .arg(newName) + .arg(suffix ? item.name : item.suffix)); + } + return false; + } + + if(suffix) + item.suffix = newName; + else + item.name = newName; + + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + + return true; +} + +bool RSModelsSQLModel::setType(RSModel &item, RsType type, RsEngineSubType subType) +{ + if(type == RsType::NTypes) + type = item.type; + else + subType = item.sub_type; + + if(type != RsType::Engine) + subType = RsEngineSubType::Invalid; //Only engines can have a subType for now + + command set_type(mDb, "UPDATE rs_models SET type=?,sub_type=? WHERE id=?"); + set_type.bind(1, int(type)); + set_type.bind(2, int(subType)); + set_type.bind(3, item.modelId); + if(set_type.execute() != SQLITE_OK) + return false; + + item.type = type; + item.sub_type = subType; + + if(sortColumn == TypeCol) + { + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + } + + return true; +} + +bool RSModelsSQLModel::removeRSModel(db_id modelId, const QString& name) +{ + if(!modelId) + return false; + + command cmd(mDb, "DELETE FROM rs_models WHERE id=?"); + cmd.bind(1, modelId); + int ret = cmd.execute(); + if(ret != SQLITE_OK) + { + ret = mDb.extended_error_code(); + if(ret == SQLITE_CONSTRAINT_TRIGGER) + { + QString tmp = name; + if(name.isNull()) + { + query q(mDb, "SELECT name FROM rs_models WHERE id=?"); + q.bind(1, modelId); + q.step(); + tmp = q.getRows().get(0); + } + + emit modelError(tr(errorModelInUseCannotDelete).arg(tmp)); + return false; + } + qWarning() << "RSModelsSQLModel: error removing model" << ret << mDb.error_msg(); + return false; + } + + refreshData(); + return true; +} + +bool RSModelsSQLModel::removeRSModelAt(int row) +{ + if(row >= curItemCount || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + const RSModel &item = cache.at(row - cacheFirstRow); + + return removeRSModel(item.modelId, item.name); +} + +db_id RSModelsSQLModel::addRSModel(int *outRow, db_id sourceModelId, const QString& suffix, QString *errOut) +{ + db_id modelId = 0; + + command cmd(mDb); + if(sourceModelId) + { + cmd.prepare("INSERT INTO rs_models(id,name,suffix,max_speed,axes,type,sub_type)" + "SELECT NULL,name,?,max_speed,axes,type,sub_type FROM rs_models WHERE id=?"); + cmd.bind(1, suffix); + cmd.bind(2, sourceModelId); + }else{ + cmd.prepare("INSERT INTO rs_models(id,name,suffix,max_speed,axes,type,sub_type) VALUES (NULL,'','',120,4,0,0)"); + } + + sqlite3_mutex *mutex = sqlite3_db_mutex(mDb.db()); + sqlite3_mutex_enter(mutex); + int ret = cmd.execute(); + if(ret == SQLITE_OK) + { + modelId = mDb.last_insert_rowid(); + } + sqlite3_mutex_leave(mutex); + + if(outRow) + *outRow = modelId ? 0 : -1; //Empty name is always the first + + if(ret == SQLITE_CONSTRAINT_UNIQUE) + { + if(sourceModelId) + { + //Error: suffix is already used + if(errOut) + *errOut = tr("Suffix is already used. Suffix must be different among models of same name."); + return 0; + } + + //There is already an empty model, use that instead + query findEmpty(mDb, "SELECT id FROM rs_models WHERE name='' OR name IS NULL LIMIT 1"); + if(findEmpty.step() == SQLITE_ROW) + { + modelId = findEmpty.getRows().get(0); + if(outRow) + *outRow = modelId ? 0 : -1; //Empty name is always the first + } + } + else if(ret != SQLITE_OK) + { + QString msg = mDb.error_msg(); + if(errOut) + *errOut = msg; + qDebug() << "RS Model Error adding:" << ret << msg << mDb.error_code() << mDb.extended_error_code(); + } + + refreshData(); //Recalc row count + switchToPage(0); //Reset to first page and so it is shown as first row + + return modelId; +} + +db_id RSModelsSQLModel::getModelIdAtRow(int row) const +{ + if(row >= curItemCount || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return 0; //Not fetched yet or invalid + const RSModel &item = cache.at(row - cacheFirstRow); + return item.modelId; +} + +bool RSModelsSQLModel::removeAllRSModels() +{ + command cmd(mDb, "DELETE FROM rs_models"); + int ret = cmd.execute(); + if(ret != SQLITE_OK) + { + qWarning() << "Removing ALL RS MODELS:" << ret << mDb.extended_error_code() << "Err:" << mDb.error_msg(); + return false; + } + + refreshData(); + return true; +} diff --git a/src/rollingstock/rsmodelssqlmodel.h b/src/rollingstock/rsmodelssqlmodel.h new file mode 100644 index 0000000..46362aa --- /dev/null +++ b/src/rollingstock/rsmodelssqlmodel.h @@ -0,0 +1,92 @@ +#ifndef RSMODELSSQLMODEL_H +#define RSMODELSSQLMODEL_H + +#include "utils/sqldelegate/pageditemmodel.h" + +#include "utils/types.h" + +#include + +class RSModelsSQLModel : public IPagedItemModel +{ + Q_OBJECT + +public: + + enum { BatchSize = 100 }; + + typedef enum { + Name = 0, + Suffix, + MaxSpeed, + Axes, + TypeCol, + SubTypeCol, + NCols + } Columns; + + typedef struct RSModel_ + { + db_id modelId; + QString name; + QString suffix; + qint16 maxSpeedKmH; + qint8 axes; + RsType type; + RsEngineSubType sub_type; + } RSModel; + + RSModelsSQLModel(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; + + // Editable: + bool setData(const QModelIndex &idx, const QVariant &value, + int role = Qt::EditRole) override; + + Qt::ItemFlags flags(const QModelIndex& idx) 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; + + // RSModelsSQLModel: + bool removeRSModel(db_id modelId, const QString &name); + bool removeRSModelAt(int row); + db_id addRSModel(int *outRow, db_id sourceModelId, const QString &suffix, QString *errOut); + + db_id getModelIdAtRow(int row) const; + + bool removeAllRSModels(); + +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); + + bool setNameOrSuffix(RSModel &item, const QString &newName, bool suffix); + bool setType(RSModel &item, RsType type, RsEngineSubType subType); + +private: + QVector cache; + int cacheFirstRow; + int firstPendingRow; +}; + +#endif // RSMODELSSQLMODEL_H diff --git a/src/rollingstock/rsownersmatchmodel.cpp b/src/rollingstock/rsownersmatchmodel.cpp new file mode 100644 index 0000000..13b9119 --- /dev/null +++ b/src/rollingstock/rsownersmatchmodel.cpp @@ -0,0 +1,118 @@ +#include "rsownersmatchmodel.h" + +RSOwnersMatchModel::RSOwnersMatchModel(database &db, QObject *parent) : + ISqlFKMatchModel(parent), + mDb(db), + q_getMatches(mDb) +{ + q_getMatches.prepare("SELECT id,name FROM rs_owners WHERE name LIKE ? LIMIT " QT_STRINGIFY(MaxMatchItems)); +} + +QVariant RSOwnersMatchModel::data(const QModelIndex &idx, int role) const +{ + if (!idx.isValid() || idx.row() >= size) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + { + if(isEmptyRow(idx.row())) + { + return ISqlFKMatchModel::tr("Empty"); + } + else if(isEllipsesRow(idx.row())) + { + return ellipsesString; + } + + return items[idx.row()].name; + } + case Qt::FontRole: + { + if(isEmptyRow(idx.row())) + { + return boldFont(); + } + break; + } + } + + return QVariant(); +} + +void RSOwnersMatchModel::autoSuggest(const QString &text) +{ + mQuery.clear(); + if(!text.isEmpty()) + { + mQuery.clear(); + mQuery.reserve(text.size() + 2); + mQuery.append('%'); + mQuery.append(text.toUtf8()); + mQuery.append('%'); + } + + refreshData(); +} + +void RSOwnersMatchModel::refreshData() +{ + if(!mDb.db()) + return; + + beginResetModel(); + + char emptyQuery = '%'; + + if(mQuery.isEmpty()) + sqlite3_bind_text(q_getMatches.stmt(), 1, &emptyQuery, 1, SQLITE_STATIC); + else + sqlite3_bind_text(q_getMatches.stmt(), 1, mQuery, mQuery.size(), SQLITE_STATIC); + + + auto end = q_getMatches.end(); + auto it = q_getMatches.begin(); + int i = 0; + for(; i < MaxMatchItems && it != end; i++) + { + items[i].ownerId = (*it).get(0); + items[i].name = (*it).get(1); + ++it; + } + + size = i + 1; //Items + Empty + + if(it != end) + { + //There would be still rows, show Ellipses + size++; //Items + Empty + Ellispses + } + + q_getMatches.reset(); + endResetModel(); + + emit resultsReady(false); +} + +QString RSOwnersMatchModel::getName(db_id id) const +{ + if(!mDb.db()) + return QString(); + + query q(mDb, "SELECT name FROM rs_owners WHERE id=?"); + q.bind(1, id); + if(q.step() == SQLITE_ROW) + return q.getRows().get(0); + return QString(); +} + +db_id RSOwnersMatchModel::getIdAtRow(int row) const +{ + return items[row].ownerId; +} + +QString RSOwnersMatchModel::getNameAtRow(int row) const +{ + return items[row].name; +} diff --git a/src/rollingstock/rsownersmatchmodel.h b/src/rollingstock/rsownersmatchmodel.h new file mode 100644 index 0000000..5505486 --- /dev/null +++ b/src/rollingstock/rsownersmatchmodel.h @@ -0,0 +1,43 @@ +#ifndef RSOWNERSMATCHMODEL_H +#define RSOWNERSMATCHMODEL_H + +#include "utils/sqldelegate/isqlfkmatchmodel.h" + +#include "utils/types.h" + +#include +using namespace sqlite3pp; + +class RSOwnersMatchModel : public ISqlFKMatchModel +{ + Q_OBJECT + +public: + RSOwnersMatchModel(database &db, QObject *parent = nullptr); + + // Basic functionality: + QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override; + + // ISqlFKMatchModel: + void autoSuggest(const QString& text) override; + virtual void refreshData() override; + QString getName(db_id id) const override; + + db_id getIdAtRow(int row) const override; + QString getNameAtRow(int row) const override; + +private: + struct RSOwnerItem + { + db_id ownerId; + QString name; + char padding[4]; + }; + RSOwnerItem items[MaxMatchItems]; + + database &mDb; + query q_getMatches; + QByteArray mQuery; +}; + +#endif // RSOWNERSMATCHMODEL_H diff --git a/src/rollingstock/rsownerssqlmodel.cpp b/src/rollingstock/rsownerssqlmodel.cpp new file mode 100644 index 0000000..4b7587e --- /dev/null +++ b/src/rollingstock/rsownerssqlmodel.cpp @@ -0,0 +1,557 @@ +#include "rsownerssqlmodel.h" + +#include + +#include +using namespace sqlite3pp; + +#include "utils/rs_types_names.h" + +#include "utils/worker_event_types.h" + +#include + +class RSOwnersResultEvent : public QEvent +{ +public: + static constexpr Type _Type = Type(CustomEvents::RsOwnersModelResult); + + inline RSOwnersResultEvent() : QEvent(_Type) {} + + QVector items; + int firstRow; +}; + +static constexpr char +errorOwnerNameAlreadyUsed[] = QT_TRANSLATE_NOOP("RSOwnersSQLModel", + "This owner name (%1) is already used."); + +static constexpr char +errorOwnerInUseCannotDelete[] = QT_TRANSLATE_NOOP("RSOwnersSQLModel", + "There are rollingstock pieces of owner %1 so it cannot be removed.\n" + "If you wish to remove it, please first delete all %1 pieces."); + +RSOwnersSQLModel::RSOwnersSQLModel(sqlite3pp::database &db, QObject *parent) : + IPagedItemModel(500, db, parent), + cacheFirstRow(0), + firstPendingRow(-BatchSize) +{ + sortColumn = Name; +} + +bool RSOwnersSQLModel::event(QEvent *e) +{ + if(e->type() == RSOwnersResultEvent::_Type) + { + RSOwnersResultEvent *ev = static_cast(e); + ev->setAccepted(true); + + handleResult(ev->items, ev->firstRow); + + return true; + } + + return QAbstractTableModel::event(e); +} + +QVariant RSOwnersSQLModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(role == Qt::DisplayRole) + { + if(orientation == Qt::Horizontal) + { + switch (section) + { + case Name: + return RsTypeNames::tr("Name"); + default: + break; + } + } + else + { + return section + curPage * ItemsPerPage + 1; + } + } + return IPagedItemModel::headerData(section, orientation, role); +} + +int RSOwnersSQLModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : curItemCount; +} + +int RSOwnersSQLModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant RSOwnersSQLModel::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 RSOwner& item = cache.at(row - cacheFirstRow); + + switch (role) + { + case Qt::DisplayRole: + case Qt::EditRole: + { + switch (idx.column()) + { + case Name: + return item.name; + } + + break; + } + } + + return QVariant(); +} + +bool RSOwnersSQLModel::setData(const QModelIndex &idx, const QVariant &value, int role) +{ + const int row = idx.row(); + if(!idx.isValid() || row >= curItemCount || idx.column() >= NCols || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + RSOwner &item = cache[row - cacheFirstRow]; + QModelIndex first = idx; + QModelIndex last = idx; + + switch (role) + { + case Qt::EditRole: + { + switch (idx.column()) + { + case Name: + { + QString newName = value.toString().simplified(); + if(item.name == newName) + return false; //No change + + command set_name(mDb, "UPDATE rs_owners SET name=? WHERE id=?"); + + if(newName.isEmpty()) + set_name.bind(1); //Bind NULL + else + set_name.bind(1, newName); + set_name.bind(2, item.ownerId); + int ret = set_name.execute(); + if(ret == SQLITE_CONSTRAINT_UNIQUE) + { + emit modelError(tr(errorOwnerNameAlreadyUsed).arg(newName)); + return false; + } + else if(ret != SQLITE_OK) + { + return false; + } + + item.name = newName; + //FIXME: maybe emit some signals? + + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + + break; + } + default: + return false; + } + break; + } + default: + return false; + } + + emit dataChanged(first, last); + return true; +} + +Qt::ItemFlags RSOwnersSQLModel::flags(const QModelIndex &idx) const +{ + if (!idx.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren; + if(idx.row() < cacheFirstRow || idx.row() >= cacheFirstRow + cache.size()) + return f; //Not fetched yet + f.setFlag(Qt::ItemIsEditable); + return f; +} + +void RSOwnersSQLModel::clearCache() +{ + cache.clear(); + cache.squeeze(); + cacheFirstRow = 0; +} + +void RSOwnersSQLModel::refreshData() +{ + if(!mDb.db()) + return; + + //TODO: consider filters + query q(mDb, "SELECT COUNT(1) FROM rs_owners"); + 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 RSOwnersSQLModel::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 RSOwnersSQLModel::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 id,name FROM rs_owners"; + 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"; + } + + 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); + + 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; + RSOwner &item = vec[i]; + item.ownerId = 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; + RSOwner &item = vec[i]; + item.ownerId = r.get(0); + item.name = r.get(1); + i++; + } + if(i < BatchSize) + vec.remove(i, BatchSize - i); + } + + + RSOwnersResultEvent *ev = new RSOwnersResultEvent; + ev->items = vec; + ev->firstRow = first; + + qApp->postEvent(this, ev); +} + +void RSOwnersSQLModel::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); + + qDebug() << "TOTAL: From:" << cacheFirstRow << "To:" << cacheFirstRow + cache.size() - 1; +} + +void RSOwnersSQLModel::setSortingColumn(int /*col*/) +{ + //Only sort by name +} + +bool RSOwnersSQLModel::removeRSOwner(db_id ownerId, const QString& name) +{ + if(!ownerId) + return false; + + command cmd(mDb, "DELETE FROM rs_owners WHERE id=?"); + cmd.bind(1, ownerId); + int ret = cmd.execute(); + if(ret != SQLITE_OK) + { + ret = mDb.extended_error_code(); + if(ret == SQLITE_CONSTRAINT_TRIGGER) + { + QString tmp = name; + if(name.isNull()) + { + query q(mDb, "SELECT name FROM rs_owners WHERE id=?"); + q.bind(1, ownerId); + q.step(); + tmp = q.getRows().get(0); + } + + emit modelError(tr(errorOwnerInUseCannotDelete).arg(tmp)); + return false; + } + qWarning() << "RSOwnersSQLModel: error removing owner" << ret << mDb.error_msg(); + return false; + } + + refreshData(); + return true; +} + +bool RSOwnersSQLModel::removeRSOwnerAt(int row) +{ + if(row >= curItemCount || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + const RSOwner &item = cache.at(row - cacheFirstRow); + + return removeRSOwner(item.ownerId, item.name); +} + +db_id RSOwnersSQLModel::addRSOwner(int *outRow) +{ + db_id ownerId = 0; + + command cmd(mDb, "INSERT INTO rs_owners(id,name) VALUES (NULL,'')"); + sqlite3_mutex *mutex = sqlite3_db_mutex(mDb.db()); + sqlite3_mutex_enter(mutex); + int ret = cmd.execute(); + if(ret == SQLITE_OK) + { + ownerId = mDb.last_insert_rowid(); + } + sqlite3_mutex_leave(mutex); + + if(ret == SQLITE_CONSTRAINT_UNIQUE) + { + //There is already an empty owner, use that instead + query findEmpty(mDb, "SELECT id FROM rs_owners WHERE name='' OR name IS NULL LIMIT 1"); + if(findEmpty.step() == SQLITE_ROW) + { + ownerId = findEmpty.getRows().get(0); + } + } + else if(ret != SQLITE_OK) + { + qDebug() << "RS Owner Error adding:" << ret << mDb.error_msg() << mDb.error_code() << mDb.extended_error_code(); + } + + refreshData(); //Recalc row count + switchToPage(0); //Reset to first page and so it is shown as first row + + if(outRow) + *outRow = ownerId ? 0 : -1; //Empty name is always the first + + return ownerId; +} + +bool RSOwnersSQLModel::removeAllRSOwners() +{ + command cmd(mDb, "DELETE FROM rs_owners"); + int ret = cmd.execute(); + if(ret != SQLITE_OK) + { + qWarning() << "Removing ALL RS OWNERS:" << ret << mDb.extended_error_code() << "Err:" << mDb.error_msg(); + return false; + } + + refreshData(); + return true; +} diff --git a/src/rollingstock/rsownerssqlmodel.h b/src/rollingstock/rsownerssqlmodel.h new file mode 100644 index 0000000..ee9c570 --- /dev/null +++ b/src/rollingstock/rsownerssqlmodel.h @@ -0,0 +1,79 @@ +#ifndef RSOWNERSSQLMODEL_H +#define RSOWNERSSQLMODEL_H + +#include "utils/sqldelegate/pageditemmodel.h" + +#include "utils/types.h" + +#include + + +class RSOwnersSQLModel : public IPagedItemModel +{ + Q_OBJECT + +public: + + enum { BatchSize = 100 }; + + typedef enum { + Name = 0, + NCols + } Columns; + + typedef struct RSOwner_ + { + db_id ownerId; + QString name; + } RSOwner; + + RSOwnersSQLModel(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; + + // Editable: + bool setData(const QModelIndex &idx, const QVariant &value, + int role = Qt::EditRole) override; + + Qt::ItemFlags flags(const QModelIndex& idx) 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; + + // RSOwnersSQLModel + + bool removeRSOwner(db_id ownerId, const QString &name); + bool removeRSOwnerAt(int row); + db_id addRSOwner(int *outRow); + + bool removeAllRSOwners(); + +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: + QVector cache; + int cacheFirstRow; + int firstPendingRow; +}; + +#endif // RSOWNERSSQLMODEL_H diff --git a/src/searchbox/CMakeLists.txt b/src/searchbox/CMakeLists.txt new file mode 100644 index 0000000..51197b9 --- /dev/null +++ b/src/searchbox/CMakeLists.txt @@ -0,0 +1,12 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + searchbox/searchresultevent.h + searchbox/searchresultitem.h + searchbox/searchresultmodel.h + searchbox/searchtask.h + + searchbox/searchresultevent.cpp + searchbox/searchresultmodel.cpp + searchbox/searchtask.cpp + PARENT_SCOPE +) diff --git a/src/searchbox/searchresultevent.cpp b/src/searchbox/searchresultevent.cpp new file mode 100644 index 0000000..735aaa7 --- /dev/null +++ b/src/searchbox/searchresultevent.cpp @@ -0,0 +1,20 @@ +#ifdef SEARCHBOX_MODE_ASYNC + +#include "searchresultevent.h" + +#include "searchresultitem.h" + +SearchResultEvent::SearchResultEvent(SearchTask *ta, const QVector &vec) : + QEvent(_Type), + task(ta), + results(vec) +{ + +} + +SearchResultEvent::~SearchResultEvent() +{ + +} + +#endif //SEARCHBOX_MODE_ASYNC diff --git a/src/searchbox/searchresultevent.h b/src/searchbox/searchresultevent.h new file mode 100644 index 0000000..0e0a7f8 --- /dev/null +++ b/src/searchbox/searchresultevent.h @@ -0,0 +1,30 @@ +#ifndef SEARCHRESULTEVENT_H +#define SEARCHRESULTEVENT_H + +#ifdef SEARCHBOX_MODE_ASYNC + +#include + +#include + +#include "utils/worker_event_types.h" + +class SearchTask; +typedef struct SearchResultItem_ SearchResultItem; + +class SearchResultEvent : public QEvent +{ +public: + static const Type _Type = Type(CustomEvents::SearchBoxResults); + + SearchResultEvent(SearchTask *ta, const QVector &vec); + virtual ~SearchResultEvent(); + +public: + SearchTask *task; + QVector results; +}; + +#endif //SEARCHBOX_MODE_ASYNC + +#endif // SEARCHRESULTEVENT_H diff --git a/src/searchbox/searchresultitem.h b/src/searchbox/searchresultitem.h new file mode 100644 index 0000000..592e8f4 --- /dev/null +++ b/src/searchbox/searchresultitem.h @@ -0,0 +1,12 @@ +#ifndef SEARCHRESULTITEM_H +#define SEARCHRESULTITEM_H + +#include "utils/types.h" + +typedef struct SearchResultItem_ +{ + db_id jobId; + JobCategory category; +} SearchResultItem; + +#endif // SEARCHRESULTITEM_H diff --git a/src/searchbox/searchresultmodel.cpp b/src/searchbox/searchresultmodel.cpp new file mode 100644 index 0000000..41002f5 --- /dev/null +++ b/src/searchbox/searchresultmodel.cpp @@ -0,0 +1,275 @@ +#include "searchresultmodel.h" + +#include "searchresultitem.h" + +#include "app/session.h" +#include "utils/jobcategorystrings.h" + +#ifdef SEARCHBOX_MODE_ASYNC + +#ifndef ENABLE_BACKGROUND_MANAGER +#error "Cannot use SEARCHBOX_MODE_ASYNC without ENABLE_BACKGROUND_MANAGER" +#endif + +#include "searchtask.h" +#include "searchresultevent.h" +#include + +#include "app/session.h" +#include "backgroundmanager/backgroundmanager.h" +#endif + +SearchResultModel::SearchResultModel(sqlite3pp::database &db, QObject *parent) : + ISqlFKMatchModel(parent), + mDb(db), + reusableTask(nullptr), + deleteReusableTaskTimerId(0) +{ + m_font.setPointSize(13); + setHasEmptyRow(false); + connect(Session->getBackgroundManager(), &BackgroundManager::abortTrivialTasks, this, &SearchResultModel::stopAllTasks); +} + +SearchResultModel::~SearchResultModel() +{ +#ifdef SEARCHBOX_MODE_ASYNC + stopAllTasks(); +#endif +} + +int SearchResultModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant SearchResultModel::data(const QModelIndex &idx, int role) const +{ + if(!idx.isValid() || idx.row() >= size) + return QVariant(); + + const SearchResultItem& item = m_data.at(idx.row()); + + switch (role) + { + case Qt::DisplayRole: + { + switch (idx.column()) + { + case JobCategoryCol: + { + if(isEllipsesRow(idx.row())) + { + break; + } + return JobCategoryName::shortName(item.category); + } + case JobNumber: + { + if(isEllipsesRow(idx.row())) + { + return ellipsesString; + } + return item.jobId; + } + } + break; + } + case Qt::ForegroundRole: + { + if(isEllipsesRow(idx.row())) + { + break; + } + switch (idx.column()) + { + case JobCategoryCol: + { + return Session->colorForCat(item.category); + } + case JobNumber: + return QColor(Qt::black); + } + break; + } + case Qt::FontRole: + { + switch (idx.column()) + { + case JobCategoryCol: + { + QFont f = m_font; + f.setWeight(QFont::Bold); + return f; + } + case JobNumber: + return m_font; + } + } + } + + return QVariant(); +} + +void SearchResultModel::autoSuggest(const QString &text) +{ +#ifdef SEARCHBOX_MODE_ASYNC + abortSearch(); //Stop previous search + + SearchTask *task = createTask(text); + tasks.append(task); + + QThreadPool::globalInstance()->start(task); +#else + + searchFor(editor->text()); //TODO +#endif +} + +void SearchResultModel::clearCache() +{ + beginResetModel(); + m_data.clear(); + m_data.squeeze(); + size = 0; + endResetModel(); +} + +db_id SearchResultModel::getIdAtRow(int row) const +{ + if(row >= m_data.size()) + return 0; + return m_data.at(row).jobId; +} + +QString SearchResultModel::getNameAtRow(int row) const +{ + if(row >= m_data.size()) + return QString(); + const SearchResultItem& item = m_data.at(row); + return JobCategoryName::jobName(item.jobId, item.category); +} + +#ifdef SEARCHBOX_MODE_ASYNC +void SearchResultModel::abortSearch() +{ + if(!tasks.isEmpty()) + tasks.last()->stop(); +} + +void SearchResultModel::stopAllTasks() +{ + for(SearchTask *task : tasks) + { + task->stop(); + task->cleanup(); + } + tasks.clear(); + clearReusableTask(); +} + +void SearchResultModel::disposeTask(SearchTask *task) +{ + if(reusableTask) + { + delete task; //We already have a reusable task + }else{ + reusableTask = task; + deleteReusableTaskTimerId = startTimer(3000, Qt::VeryCoarseTimer); //3 sec, then delete + } +} + +SearchTask *SearchResultModel::createTask(const QString& text) +{ + if(catNames.isEmpty()) + { + //Load categories names + catNames.reserve(int(JobCategory::NCategories)); + for(int cat = 0; cat < int(JobCategory::NCategories); cat++) + { + catNames.append(JobCategoryName::tr(JobCategoryFullNameTable[cat])); + } + + catNamesAbbr.reserve(int(JobCategory::NCategories)); + for(int cat = 0; cat < int(JobCategory::NCategories); cat++) + { + catNamesAbbr.append(JobCategoryName::tr(JobCategoryAbbrNameTable[cat])); + } + } + + SearchTask *task; + if(reusableTask) + { + killTimer(deleteReusableTaskTimerId); + deleteReusableTaskTimerId = 0; + task = reusableTask; + reusableTask = nullptr; + + //NOTE: SearchTask gets disposed after finishing running so IQuittableTask has set mReceiver to nullptr + task->setReceiver(this); + } + else + { + task = new SearchTask(this, MaxMatchItems + 1, mDb, catNames, catNamesAbbr); + } + task->setQuery(text); + return task; +} + +void SearchResultModel::clearReusableTask() +{ + if(deleteReusableTaskTimerId) + { + killTimer(deleteReusableTaskTimerId); + deleteReusableTaskTimerId = 0; + } + if(reusableTask) + { + delete reusableTask; + reusableTask = nullptr; + } + if(tasks.isEmpty()) + { + catNames.clear(); + catNamesAbbr.clear(); + } +} + +bool SearchResultModel::event(QEvent *ev) +{ + if(ev->type() == QEvent::Timer && static_cast(ev)->timerId() == deleteReusableTaskTimerId) + { + clearReusableTask(); + return true; + } + if(ev->type() == SearchResultEvent::_Type) + { + SearchResultEvent *e = static_cast(ev); + e->setAccepted(true); + int idx = tasks.indexOf(e->task); + if(idx != -1) + { + bool wasLast = idx == tasks.size() - 1; + tasks.removeAt(idx); + disposeTask(e->task); + + if(wasLast) + { + beginResetModel(); + m_data = e->results; + + if(m_data.size() > MaxMatchItems) + size = MaxMatchItems + 1; //There would be still rows, show Ellipses + else + size = m_data.size(); + + endResetModel(); + emit resultsReady(true); + } + } + + return true; + } + + return QObject::event(ev); +} +#endif diff --git a/src/searchbox/searchresultmodel.h b/src/searchbox/searchresultmodel.h new file mode 100644 index 0000000..3d982f9 --- /dev/null +++ b/src/searchbox/searchresultmodel.h @@ -0,0 +1,78 @@ +#ifndef SEARCHRESULTMODEL_H +#define SEARCHRESULTMODEL_H + +#include "utils/sqldelegate/isqlfkmatchmodel.h" + +#include + +#include + +#include "utils/types.h" + +#include "searchresultitem.h" + +#ifdef SEARCHBOX_MODE_ASYNC +class SearchTask; +#endif + +namespace sqlite3pp { +class database; +} + +class SearchResultModel : public ISqlFKMatchModel +{ + Q_OBJECT + +public: + enum Column + { + JobCategoryCol = 0, + JobNumber, + NCols + }; + + SearchResultModel(sqlite3pp::database &db, QObject *parent = nullptr); + ~SearchResultModel(); + +#ifdef SEARCHBOX_MODE_ASYNC + bool event(QEvent *ev) override; +#endif + + // Basic functionality: + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + // ISqlFKMatchModel + virtual void autoSuggest(const QString& text) override; + virtual void clearCache() override; + + virtual db_id getIdAtRow(int row) const override; + virtual QString getNameAtRow(int row) const override; + +public slots: +#ifdef SEARCHBOX_MODE_ASYNC + void abortSearch(); + void stopAllTasks(); +#endif + +private: + sqlite3pp::database &mDb; + +#ifdef SEARCHBOX_MODE_ASYNC + QVector tasks; + SearchTask *reusableTask; + int deleteReusableTaskTimerId; + + void disposeTask(SearchTask *task); + SearchTask *createTask(const QString &text); + void clearReusableTask(); +#endif + + QVector m_data; + + QFont m_font; + + QStringList catNames, catNamesAbbr; +}; + +#endif // SEARCHRESULTMODEL_H diff --git a/src/searchbox/searchtask.cpp b/src/searchbox/searchtask.cpp new file mode 100644 index 0000000..448beb5 --- /dev/null +++ b/src/searchbox/searchtask.cpp @@ -0,0 +1,205 @@ +#ifdef SEARCHBOX_MODE_ASYNC + +#include "searchtask.h" +#include "searchresultitem.h" +#include "searchresultevent.h" + +#include + +#include "app/session.h" + +#include + +SearchTask::SearchTask(QObject *receiver, int limitRows, sqlite3pp::database &db, + const QStringList &catNames_, const QStringList &catNamesAbbr_) : + IQuittableTask(receiver), + mDb(db), + q_selectJobs(mDb), + regExp(nullptr), + limitResultRows(limitRows), + queryType(Invalid), + catNames(catNames_), + catNamesAbbr(catNamesAbbr_) +{ +} + +SearchTask::~SearchTask() +{ + if(regExp) + { + delete regExp; + regExp = nullptr; + } +} + +void SearchTask::run() +{ + QVector results; + + if(!regExp) + regExp = new QRegularExpression("(?[^0-9\\s]*)\\s*(?\\d*)"); + + QRegularExpressionMatch match = regExp->match(mQuery); + if(!match.hasMatch()) + { + sendEvent(new SearchResultEvent(this, results), true); + return; + } + + QString name = match.captured("name"); + QString num = match.captured("num"); + + if(wasStopped()) + { + sendEvent(new SearchResultEvent(this, results), true); + return; + } + + if(!name.isEmpty()) + { + //Find the matching category + name = name.toUpper(); + QList categories; + + int cat = catNamesAbbr.indexOf(name); + + if(cat == -1) //Retry with full names + cat = catNames.indexOf(name); + + if(cat != -1) + { + categories.append(cat); + } + else + { + //Retry with full names that start with ... (partial names) + for(int i = 0; i < catNames.size(); i++) + { + if(catNames.at(i).startsWith(name, Qt::CaseInsensitive)) + { + categories.append(i); //Don't break, allow multiple categories + } + } + } + + if(categories.isEmpty()) + return; //Failed to find a category + + if(num.isEmpty()) + { + //It's a category like 'IC' + //Match all jobs with that category + searchByCat(categories, results); + } + else + { + //Category + number + //Match all jobs beggining with this number and with this category + searchByCatAndNum(categories, num, results); + } + } + else if(!num.isEmpty()) + { + //Search all jobs beginning with this number + searchByNum(num, results); + } + + sendEvent(new SearchResultEvent(this, results), true); +} + +void SearchTask::searchByCat(const QList& categories, QVector& jobs) +{ + if(queryType != ByCat) + { + q_selectJobs.prepare("SELECT id FROM jobs WHERE category=?1 ORDER BY id LIMIT ?2"); + queryType = ByCat; + } + + for(const int cat : categories) + { + SearchResultItem item; + item.category = JobCategory(cat); + + q_selectJobs.bind(1, cat); + q_selectJobs.bind(2, limitResultRows); + + for(auto job : q_selectJobs) + { + //Check every 4 items + if(jobs.size() % 4 == 0 && wasStopped()) + return; + + item.jobId = job.get(0); + jobs.append(item); + } + q_selectJobs.reset(); + } +} + +void SearchTask::searchByCatAndNum(const QList& categories, const QString& num, QVector& jobs) +{ + if(queryType != ByCatAndNum) + { + q_selectJobs.prepare("SELECT id, id LIKE ?2 as job_rank FROM jobs" + " WHERE category=?1 AND id LIKE ?3" + " ORDER BY job_rank DESC, id ASC LIMIT ?4"); + queryType = ByCatAndNum; + } + + for(const int cat : categories) + { + SearchResultItem item; + item.category = JobCategory(cat); + + q_selectJobs.bind(1, cat); + q_selectJobs.bind(2, num + '%'); + q_selectJobs.bind(3, '%' + num + '%'); + q_selectJobs.bind(4, limitResultRows); + + for(auto job : q_selectJobs) + { + //Check every 4 items + if(jobs.size() % 4 == 0 && wasStopped()) + return; + + item.jobId = job.get(0); + jobs.append(item); + } + q_selectJobs.reset(); + } +} + +void SearchTask::searchByNum(const QString& num, QVector& jobs) +{ + if(queryType != ByNum) + { + q_selectJobs.prepare("SELECT id, category, id LIKE ?1 as job_rank FROM jobs" + " WHERE id LIKE ?2" + " ORDER BY job_rank DESC, id ASC LIMIT ?3"); + queryType = ByNum; + } + + q_selectJobs.bind(1, num + '%'); + q_selectJobs.bind(2, '%' + num + '%'); + q_selectJobs.bind(3, limitResultRows); + + for(auto job : q_selectJobs) + { + //Check every 4 items + if(jobs.size() % 4 == 0 && wasStopped()) + return; + + SearchResultItem item; + item.jobId = job.get(0); + item.category = JobCategory(job.get(1)); + jobs.append(item); + } + q_selectJobs.reset(); +} + +void SearchTask::setQuery(const QString &query) +{ + mQuery = query; +} + +#endif //SEARCHBOX_MODE_ASYNC diff --git a/src/searchbox/searchtask.h b/src/searchbox/searchtask.h new file mode 100644 index 0000000..2d7a260 --- /dev/null +++ b/src/searchbox/searchtask.h @@ -0,0 +1,53 @@ +#ifndef SEARCHTASK_H +#define SEARCHTASK_H + +#ifdef SEARCHBOX_MODE_ASYNC + +#include "utils/thread/iquittabletask.h" + +#include +#include "utils/types.h" + +#include + +typedef struct SearchResultItem_ SearchResultItem; +class QRegularExpression; + +class SearchTask : public IQuittableTask +{ +public: + SearchTask(QObject *receiver, int limitRows, sqlite3pp::database &db, + const QStringList& catNames_, const QStringList& catNamesAbbr_); + ~SearchTask(); + + void run() override; + + void setQuery(const QString &query); + +private: + void searchByCat(const QList &categories, QVector &jobs); + void searchByCatAndNum(const QList &categories, const QString &num, QVector &jobs); + void searchByNum(const QString &num, QVector &jobs); + +private: + enum QueryType + { + Invalid, + ByCat, + ByCatAndNum, + ByNum + }; + + sqlite3pp::database &mDb; + sqlite3pp::query q_selectJobs; + QRegularExpression *regExp; + + QString mQuery; + int limitResultRows; + QueryType queryType; + QStringList catNames, catNamesAbbr; //FIXME: store in search engine cache +}; + +#endif //SEARCHBOX_MODE_ASYNC + +#endif // SEARCHTASK_H diff --git a/src/settings/CMakeLists.txt b/src/settings/CMakeLists.txt new file mode 100644 index 0000000..cc675a4 --- /dev/null +++ b/src/settings/CMakeLists.txt @@ -0,0 +1,16 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + settings/appsettings.h + settings/settingsdialog.h + settings/type_utils.h + + settings/appsettings.cpp + settings/settingsdialog.cpp + PARENT_SCOPE +) + +set(TRAINTIMETABLE_UI_FILES + ${TRAINTIMETABLE_UI_FILES} + settings/settingsdialog.ui + PARENT_SCOPE +) diff --git a/src/settings/appsettings.cpp b/src/settings/appsettings.cpp new file mode 100644 index 0000000..a99996c --- /dev/null +++ b/src/settings/appsettings.cpp @@ -0,0 +1,132 @@ +#include "appsettings.h" + +#include "utils/types.h" + +#include + +#include + +#include + +TrainTimetableSettings::TrainTimetableSettings(QObject *parent) : + QObject(parent) +{ + +} + +void TrainTimetableSettings::loadSettings(const QString& fileName) +{ + m_settings.reset(new QSettings(fileName, QSettings::IniFormat)); + qDebug() << "SETTINGS:" << m_settings->fileName() << m_settings->isWritable(); +} + +void TrainTimetableSettings::saveSettings() +{ + if(m_settings) + m_settings->sync(); +} + +void TrainTimetableSettings::restoreDefaultSettings() +{ + if(!m_settings) + return; + + m_settings->clear(); +} + +QColor TrainTimetableSettings::getCategoryColor(int category) +{ + if(!m_settings || category >= int(JobCategory::NCategories) || category < 0) + return QColor(); //Invalid Category + + QRgb defaultColors[int(JobCategory::NCategories)] = { //NOTE: keep in sync wiht JobCategory + 0x00FFFF, //FREIGHT (MERCI) + 0x0000FF, //LIS (LIS) + 0x808000, //POSTAL (POSTALE) + + 0x008000, //REGIONAL (REGIONALE) + 0x005500, //FAST_REGIONAL (REGIONALE VELOCE) + 0x00FF00, //LOCAL (LOCALE) + 0xFFAA00, //INTERCITY (INTERCITY) + 0x800080, //EXPRESS (ESPRESSO) + 0x800000, //DIRECT (DIRETTO) + 0xFF0000, //HIGH_SPEED (ALTA VELOCITA') + }; + + //TODO: maybe use english category names in key instead of enum index + const QString key = QStringLiteral("job_colors/category_") + QString::number(category); + + return utils::fromVariant(m_settings->value(key, QColor(defaultColors[category]))); +} + +void TrainTimetableSettings::setCategoryColor(int category, const QColor &color) +{ + if(!m_settings || category >= int(JobCategory::NCategories) || category < 0 || !color.isValid()) + return; //Invalid Category + + //TODO: maybe use english category names in key instead of enum index + const QString key = QStringLiteral("job_colors/category_") + QString::number(category); + + m_settings->setValue(key, utils::toVariant(color)); +} + +int TrainTimetableSettings::getDefaultStopMins(int category) +{ + if(!m_settings || category >= int(JobCategory::NCategories) || category < 0) + return -1; //Invalid Category + + //0 minutes means transit + + quint8 defaultTimesArr[int(JobCategory::NCategories)] = { //NOTE: keep in sync wiht JobCategory + 10, //FREIGHT (MERCI) + 10, //LIS (LIS) + 10, //POSTAL (POSTALE) + + 2, //REGIONAL (REGIONALE) + 2, //FAST_REGIONAL (REGIONALE VELOCE) + 2, //LOCAL (LOCALE) + 0, //INTERCITY (INTERCITY) + 0, //EXPRESS (ESPRESSO) + 0, //DIRECT (DIRETTO) + 0, //HIGH_SPEED (ALTA VELOCITA') + }; + + //TODO: maybe use english category names in key instead of enum index + const QString key = QStringLiteral("stops/default_stop_mins_") + QString::number(category); + return m_settings->value(key, defaultTimesArr[category]).toInt(); +} + +void TrainTimetableSettings::setDefaultStopMins(int category, int mins) +{ + if(!m_settings || category >= int(JobCategory::NCategories) || category < 0 || mins < 0) + return; //Invalid Category + + //TODO: maybe use english category names in key instead of enum index + const QString key = QStringLiteral("stops/default_stop_mins_") + QString::number(category); + m_settings->setValue(key, mins); +} + +QFont TrainTimetableSettings::getFontHelper(const QString &baseKey, QFont defFont) +{ + //NOTE: split into 2 keys: family, pt + if(!m_settings) + return defFont; + + const int pt_size = m_settings->value(baseKey + QLatin1String("_pt")).toInt(); + const QString family = m_settings->value(baseKey + QLatin1String("_family")).toString(); + + if(pt_size != 0) + defFont.setPointSize(pt_size); + if(!family.isEmpty()) + defFont.setFamily(family); + return defFont; +} + +void TrainTimetableSettings::setFontHelper(const QString &baseKey, const QFont &f) +{ + if(!m_settings) + return; + + m_settings->setValue(baseKey + QLatin1String("_pt"), f.pointSize()); + m_settings->setValue(baseKey + QLatin1String("_family"), f.family()); +} diff --git a/src/settings/appsettings.h b/src/settings/appsettings.h new file mode 100644 index 0000000..f54b5d1 --- /dev/null +++ b/src/settings/appsettings.h @@ -0,0 +1,141 @@ +#ifndef APPSETTINGS_H +#define APPSETTINGS_H + +#include +#include +#include + +#include + +#include "type_utils.h" + +#include "info.h" + +//FIXME: check if all settings are wired to SettingsDialog + +#define FIELD_GET(name, str, type, val)\ + type get ## name ()\ + {\ + if(m_settings)\ + return utils::fromVariant(m_settings->value(QStringLiteral(str), val));\ + return val;\ + } + +#define FIELD_SET(name, str, type)\ + void set ## name (utils::const_ref_t::Type v)\ + {\ + if(!m_settings)\ + return;\ + return m_settings->setValue(QStringLiteral(str), utils::toVariant(v));\ + } + +#define FIELD(name, str, type, val)\ + FIELD_GET(name, str, type, val)\ + FIELD_SET(name, str, type) + +#define FONT_FIELD(name, str, val)\ + QFont get ## name ()\ + {\ + return getFontHelper(str, val);\ + }\ + void set ## name (const QFont& f)\ + {\ + return setFontHelper(str, f);\ + } + +class TrainTimetableSettings : public QObject +{ + Q_OBJECT +public: + explicit TrainTimetableSettings(QObject *parent = nullptr); + + void loadSettings(const QString &fileName); + + void saveSettings(); + void restoreDefaultSettings(); + + //General + FIELD(Language, "language", QLocale, QLocale(QLocale::English)) + FIELD(RecentFiles, "recent_files", QStringList, QStringList()) + + //Job Graph + FIELD(HorizontalOffset, "job_graph/horizontal_offset", int, 50) + FIELD(HourLineOffset, "job_graph/hour_line_offset", int, 45) + FIELD(VerticalOffset, "job_graph/vertical_offset", int, 50) + FIELD(HourOffset, "job_graph/hour_offset", int, 100) + FIELD(StationOffset, "job_graph/station_offset", int, 150) + FIELD(PlatformOffset, "job_graph/platform_offset", int, 12) + + FIELD(PlatformLineWidth, "job_graph/platf_line_width", int, 2) + FIELD(HourLineWidth, "job_graph/hour_line_width", int, 2) + FIELD(JobLineWidth, "job_graph/job_line_width", int, 6) + + FIELD(HourLineColor, "job_graph/hour_line_color", QColor, QColor(Qt::black)) + FIELD(HourTextColor, "job_graph/hour_text_color", QColor, QColor(Qt::green)) + FIELD(StationTextColor, "job_graph/station_text_color", QColor, QColor(Qt::red)) + FIELD(MainPlatfColor, "job_graph/main_platf_color", QColor, QColor(Qt::magenta)) + FIELD(DepotPlatfColor, "job_graph/depot_platf_color", QColor, QColor(Qt::darkGray)) + + FIELD(JobLabelFontSize, "job_graph/job_label_font_size", qreal , 12.0) + + QFont getJobLabelFont(); //TODO: settings + void setJobLabelFont(const QFont& f); + + //Job Colors + QColor getCategoryColor(int category); + void setCategoryColor(int category, const QColor& color); + + //Job Editor + FIELD(ChooseLineOnAddStop, "job_editor/choose_line_on_add_stop", bool, true) + + //Stops + FIELD(AutoInsertTransits, "job_editor/auto_insert_transits", bool, true) + FIELD(AutoShiftLastStopCouplings, "job_editor/auto_shift_couplings", bool, true) + FIELD(AutoUncoupleAtLastStop, "job_editor/auto_uncouple_at_last_stop", bool, true) + int getDefaultStopMins(int category); + void setDefaultStopMins(int category, int mins); + + //Shift Graph + FIELD(ShiftHourOffset, "shift_graph/hour_offset", double, 150.0) + FIELD(ShiftHorizOffset, "shift_graph/horiz_offset", double, 50.0) + FIELD(ShiftVertOffset, "shift_graph/vert_offset", double, 20.0) + FIELD(ShiftJobOffset, "shift_graph/job_offset", double, 50.0) + FIELD(ShiftJobBoxOffset, "shift_graph/job_box_offset", double, 20.0) + FIELD(ShiftStationOffset, "shift_graph/station_offset", double, 5.0) + FIELD(ShiftHideSameStations, "shift_graph/hide_same_stations", bool, true) + + //RollingStock + FIELD(RemoveMergedSourceModel, + "rollingstock/remove_merged_source_model", + bool, false) + FIELD(RemoveMergedSourceOwner, + "rollingstock/remove_merged_source_owner", + bool, false) + + //RS Import + FIELD(ODSFirstRow, "rs_import/first_row", int, 3) + FIELD(ODSNumCol, "rs_import/num_column", int, 1) + FIELD(ODSNameCol, "rs_import/model_column", int, 3) + + //Sheet export ODT, NOTE: header/footer can be overriden by session specific values + FIELD(SheetHeader, "sheet_export/header", QString, QString()) + FIELD(SheetFooter, "sheet_export/footer", QString, QStringLiteral("Generated by %1").arg(AppDisplayName)) + FIELD(SheetStoreLocationDateInMeta, "sheet_export/location_date_in_meta", bool, true) + + //Background Tasks + FIELD(CheckRSWhenOpeningDB, "background_tasks/check_rs_at_startup", bool, true) + FIELD(CheckRSOnJobEdit, "background_tasks/check_rs_on_job_edited", bool, true) + +signals: + void jobColorsChanged(); + void jobGraphOptionsChanged(); + void shiftGraphOptionsChanged(); + void stopOptionsChanged(); + +private: + QFont getFontHelper(const QString& baseKey, QFont defFont); + void setFontHelper(const QString& baseKey, const QFont& f); + QScopedPointer m_settings; +}; + +#endif // APPSETTINGS_H diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp new file mode 100644 index 0000000..f4a6372 --- /dev/null +++ b/src/settings/settingsdialog.cpp @@ -0,0 +1,381 @@ +#include "settingsdialog.h" +#include "ui_settingsdialog.h" + +#include "app/session.h" +#include "utils/jobcategorystrings.h" + +#include + +#include +#include + +#include + +SettingsDialog::SettingsDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::SettingsDialog), + updateJobsColors(false), + updateJobGraphOptions(false), + updateShiftGraphOptions(false) +{ + ui->setupUi(this); + + setupLanguageBox(); + + connect(this, &QDialog::accepted, this, &SettingsDialog::saveSettings); + connect(ui->restoreBut, &QPushButton::clicked, this, &SettingsDialog::onRestore); + + //Setup default stop time section + QFormLayout *lay = new QFormLayout(ui->stopDurationBox); + const QString suffix = tr("minutes"); + for(int i = 0; i < int(JobCategory::NCategories); i++) + { + QSpinBox *spin = new QSpinBox; + spin->setMinimum(0); + spin->setMaximum(5 * 60); //Maximum 5 hours + spin->setSuffix(suffix); + + lay->addRow(JobCategoryName::fullName(JobCategory(i)), spin); + m_timeSpinBoxArr[i] = spin; + } + + setupJobColorsPage(); + + connect(ui->vertOffsetSpin, static_cast(&QSpinBox::valueChanged), this, &SettingsDialog::onJobGraphOptionsChanged); + connect(ui->horizOffsetSpin, static_cast(&QSpinBox::valueChanged), this, &SettingsDialog::onJobGraphOptionsChanged); + connect(ui->hourOffsetSpin, static_cast(&QSpinBox::valueChanged), this, &SettingsDialog::onJobGraphOptionsChanged); + connect(ui->stationsOffsetSpin, static_cast(&QSpinBox::valueChanged), this, &SettingsDialog::onJobGraphOptionsChanged); + connect(ui->platformsOffsetSpin, static_cast(&QSpinBox::valueChanged), this, &SettingsDialog::onJobGraphOptionsChanged); + connect(ui->hourLineStartSpinBox, static_cast(&QSpinBox::valueChanged), this, &SettingsDialog::onJobGraphOptionsChanged); + + connect(ui->hourLineWidthSpin, static_cast(&QSpinBox::valueChanged), this, &SettingsDialog::onJobGraphOptionsChanged); + connect(ui->jobLineWidthSpin, static_cast(&QSpinBox::valueChanged), this, &SettingsDialog::onJobGraphOptionsChanged); + connect(ui->platformsLineWidthSpin, static_cast(&QSpinBox::valueChanged), this, &SettingsDialog::onJobGraphOptionsChanged); + + connect(ui->hourTextColor, &ColorView::colorChanged, this, &SettingsDialog::onJobGraphOptionsChanged); + connect(ui->hourLineColor, &ColorView::colorChanged, this, &SettingsDialog::onJobGraphOptionsChanged); + connect(ui->stationTextColor, &ColorView::colorChanged, this, &SettingsDialog::onJobGraphOptionsChanged); + connect(ui->mainPlatformColor, &ColorView::colorChanged, this, &SettingsDialog::onJobGraphOptionsChanged); + connect(ui->depotPlatformColor, &ColorView::colorChanged, this, &SettingsDialog::onJobGraphOptionsChanged); + + connect(ui->shiftHourOffsetSpin, static_cast(&QDoubleSpinBox::valueChanged), + this, &SettingsDialog::onShiftGraphOptionsChanged); + connect(ui->shiftHorizOffsetSpin, static_cast(&QDoubleSpinBox::valueChanged), + this, &SettingsDialog::onShiftGraphOptionsChanged); + connect(ui->shiftVertOffsetSpin, static_cast(&QDoubleSpinBox::valueChanged), + this, &SettingsDialog::onShiftGraphOptionsChanged); + connect(ui->shiftJobOffsetSpin, static_cast(&QDoubleSpinBox::valueChanged), + this, &SettingsDialog::onShiftGraphOptionsChanged); + connect(ui->shiftJobBoxOffsetSpin, static_cast(&QDoubleSpinBox::valueChanged), + this, &SettingsDialog::onShiftGraphOptionsChanged); + connect(ui->shiftStLabelOffsetSpin, static_cast(&QDoubleSpinBox::valueChanged), + this, &SettingsDialog::onShiftGraphOptionsChanged); + connect(ui->shiftHideSameStCheck, &QCheckBox::toggled, this, &SettingsDialog::onShiftGraphOptionsChanged); +} + +void SettingsDialog::setupJobColorsPage() +{ + //TODO: add scroll area also to other pages like stops + QScrollArea *jobColors = new QScrollArea; + ui->tabWidget->insertTab(3, jobColors, tr("Job Colors")); + + QWidget *jobColorsViewPort = new QWidget; + QVBoxLayout *jobColorsLay = new QVBoxLayout(jobColorsViewPort); + + QGroupBox *freightBox = new QGroupBox(tr("Non Passenger")); + QFormLayout *freightLay = new QFormLayout(freightBox); + + for(int cat = 0; cat < int(FirstPassengerCategory); cat++) + { + ColorView *color = new ColorView(this); + connect(color, &ColorView::colorChanged, this, &SettingsDialog::onJobColorChanged); + freightLay->addRow(JobCategoryName::fullName(JobCategory(cat)), color); + m_colorViews[cat] = color; + } + jobColorsLay->addWidget(freightBox); + + QGroupBox *passengerBox = new QGroupBox(tr("Passenger")); + QFormLayout *passengerLay = new QFormLayout(passengerBox); + + for(int cat = int(FirstPassengerCategory); cat < int(JobCategory::NCategories); cat++) + { + ColorView *color = new ColorView(this); + connect(color, &ColorView::colorChanged, this, &SettingsDialog::onJobColorChanged); + passengerLay->addRow(JobCategoryName::fullName(JobCategory(cat)), color); + m_colorViews[cat] = color; + } + jobColorsLay->addWidget(passengerBox); + + jobColors->setWidget(jobColorsViewPort); + jobColors->setWidgetResizable(true); +} + +SettingsDialog::~SettingsDialog() +{ + delete ui; +} + +void SettingsDialog::setupLanguageBox() +{ + ui->languageCombo->clear(); + + ui->languageCombo->addItem(QStringLiteral("English"), QLocale::English); + ui->languageCombo->addItem(QStringLiteral("Italiano"), QLocale::Italian); +} + +inline void set(QSpinBox *spin, int val) +{ + spin->blockSignals(true); + spin->setValue(val); + spin->blockSignals(false); +} + +inline void set(QDoubleSpinBox *spin, double val) +{ + spin->blockSignals(true); + spin->setValue(val); + spin->blockSignals(false); +} + +void SettingsDialog::loadSettings() +{ + auto &settings = AppSettings; + + //General + QLocale locale = settings.getLanguage(); + int idx = ui->languageCombo->findData(locale.language()); + if(idx < 0) + idx = ui->languageCombo->findData(QLocale::English); + + ui->languageCombo->setCurrentIndex(idx); + + //Job Graph + set(ui->hourOffsetSpin, settings.getHourOffset()); + set(ui->stationsOffsetSpin, settings.getStationOffset()); + set(ui->platformsOffsetSpin, settings.getPlatformOffset()); + set(ui->horizOffsetSpin, settings.getHorizontalOffset()); + set(ui->vertOffsetSpin, settings.getVerticalOffset()); + set(ui->hourLineStartSpinBox, settings.getHourLineOffset()); + + set(ui->hourLineWidthSpin, settings.getHourLineWidth()); + set(ui->jobLineWidthSpin, settings.getJobLineWidth()); + set(ui->platformsLineWidthSpin, settings.getPlatformLineWidth()); + + ui->hourTextColor-> setColor(settings.getHourTextColor()); + ui->hourLineColor-> setColor(settings.getHourLineColor()); + ui->stationTextColor-> setColor(settings.getStationTextColor()); + ui->mainPlatformColor-> setColor(settings.getMainPlatfColor()); + ui->depotPlatformColor->setColor(settings.getDepotPlatfColor()); + + //Job Colors + for(int cat = 0; cat < int(JobCategory::NCategories); cat++) + { + QColor col = settings.getCategoryColor(cat); + m_colorViews[cat]->setColor(col); + } + + //Stops + for(int cat = 0; cat < int(JobCategory::NCategories); cat++) + { + m_timeSpinBoxArr[cat]->blockSignals(true); + m_timeSpinBoxArr[cat]->setValue(settings.getDefaultStopMins(cat)); + m_timeSpinBoxArr[cat]->blockSignals(false); + } + ui->autoInsertTransitsCheckBox->setChecked(settings.getAutoInsertTransits()); + ui->autoMoveLastCouplingsCheck->setChecked(settings.getAutoShiftLastStopCouplings()); + ui->autoUncoupleAllAtLastStopCheck->setChecked(settings.getAutoUncoupleAtLastStop()); + + //Job Editor + ui->chooseLineBeforeLastStopBox->setChecked(settings.getChooseLineOnAddStop()); + + //Shift Graph + set(ui->shiftHourOffsetSpin, settings.getShiftHourOffset()); + set(ui->shiftHorizOffsetSpin, settings.getShiftHorizOffset()); + set(ui->shiftVertOffsetSpin, settings.getShiftVertOffset()); + set(ui->shiftJobOffsetSpin, settings.getShiftJobOffset()); + set(ui->shiftJobBoxOffsetSpin, settings.getShiftJobBoxOffset()); + set(ui->shiftStLabelOffsetSpin, settings.getShiftStationOffset()); + ui->shiftHideSameStCheck->setChecked(settings.getShiftHideSameStations()); + + //Rollingstock + ui->mergeModelRemoveCheck->setChecked(settings.getRemoveMergedSourceModel()); + ui->mergeOwnerRemoveCheck->setChecked(settings.getRemoveMergedSourceOwner()); + + //RS Import + set(ui->odsFirstRowSpin, settings.getODSFirstRow()); + set(ui->odsNumColumnSpin, settings.getODSNumCol()); + set(ui->odsModelColumnSpin, settings.getODSNameCol()); + + //Sheet Export ODT + ui->sheetExportHeaderEdit->setText(settings.getSheetHeader()); + ui->sheetExportFooterEdit->setText(settings.getSheetFooter()); + ui->sheetMetadataCheckBox->setChecked(settings.getSheetStoreLocationDateInMeta()); + + //Background Tasks + ui->rsErrCheckAtFileOpen->setChecked(settings.getCheckRSWhenOpeningDB()); + ui->rsErrCheckOnJobEdited->setChecked(settings.getCheckRSOnJobEdit()); + + updateJobsColors = false; + updateJobGraphOptions = false; +} + +void SettingsDialog::saveSettings() +{ + auto &settings = AppSettings; + + //General + QVariant v = ui->languageCombo->currentData(); + QLocale::Language lang = v.isValid() ? v.value() : QLocale::English; + settings.setLanguage(QLocale(lang)); + + //Job Graph + settings.setHourOffset(ui->hourOffsetSpin->value()); + settings.setStationOffset(ui->stationsOffsetSpin->value()); + settings.setPlatformOffset(ui->platformsOffsetSpin->value()); + settings.setHorizontalOffset(ui->horizOffsetSpin->value()); + settings.setVerticalOffset(ui->vertOffsetSpin->value()); + settings.setHourLineOffset(ui->hourLineStartSpinBox->value()); + + settings.setHourLineWidth(ui->hourLineWidthSpin->value()); + settings.setJobLineWidth(ui->jobLineWidthSpin->value()); + settings.setPlatformLineWidth(ui->platformsLineWidthSpin->value()); + + settings.setHourTextColor(ui->hourTextColor->color()); + settings.setHourLineColor(ui->hourLineColor->color()); + settings.setStationTextColor(ui->stationTextColor->color()); + settings.setMainPlatfColor(ui->mainPlatformColor->color()); + settings.setDepotPlatfColor(ui->depotPlatformColor->color()); + + //Job Colors + for(int cat = 0; cat < int(JobCategory::NCategories); cat++) + { + settings.setCategoryColor(cat, m_colorViews[cat]->color()); + } + + //Stops + for(int cat = 0; cat < int(JobCategory::NCategories); cat++) + { + settings.setDefaultStopMins(cat, m_timeSpinBoxArr[cat]->value()); + } + bool newVal = ui->autoInsertTransitsCheckBox->isChecked(); + bool stopSetingsChanged = settings.getAutoInsertTransits() != newVal; + settings.setAutoInsertTransits(newVal); + + newVal = ui->autoMoveLastCouplingsCheck->isChecked(); + stopSetingsChanged |= settings.getAutoShiftLastStopCouplings() != newVal; + settings.setAutoShiftLastStopCouplings(newVal); + + newVal = ui->autoUncoupleAllAtLastStopCheck->isChecked(); + stopSetingsChanged |= settings.getAutoUncoupleAtLastStop() != newVal; + settings.setAutoUncoupleAtLastStop(newVal); + + //Job Editor + settings.setChooseLineOnAddStop(ui->chooseLineBeforeLastStopBox->isChecked()); + + //Shift Graph + settings.setShiftHourOffset(ui->shiftHourOffsetSpin->value()); + settings.setShiftHorizOffset(ui->shiftHorizOffsetSpin->value()); + settings.setShiftVertOffset(ui->shiftVertOffsetSpin->value()); + settings.setShiftJobOffset(ui->shiftJobOffsetSpin->value()); + settings.setShiftJobBoxOffset(ui->shiftJobBoxOffsetSpin->value()); + settings.setShiftStationOffset(ui->shiftStLabelOffsetSpin->value()); + settings.setShiftHideSameStations(ui->shiftHideSameStCheck->isChecked()); + + //Rollingstock + settings.setRemoveMergedSourceModel(ui->mergeModelRemoveCheck->isChecked()); + settings.setRemoveMergedSourceOwner(ui->mergeOwnerRemoveCheck->isChecked()); + + //RS Import + settings.setODSFirstRow(ui->odsFirstRowSpin->value()); + settings.setODSNumCol(ui->odsNumColumnSpin->value()); + settings.setODSNameCol(ui->odsModelColumnSpin->value()); + + //Sheet Export ODT + settings.setSheetHeader(ui->sheetExportHeaderEdit->text()); + settings.setSheetFooter(ui->sheetExportFooterEdit->text()); + settings.setSheetStoreLocationDateInMeta(ui->sheetMetadataCheckBox->isChecked()); + + //Background Tasks + settings.setCheckRSWhenOpeningDB(ui->rsErrCheckAtFileOpen->isChecked()); + settings.setCheckRSOnJobEdit(ui->rsErrCheckOnJobEdited->isChecked()); + + settings.saveSettings(); //Sync to file + + if(updateJobGraphOptions) + { + emit settings.jobGraphOptionsChanged(); + updateJobGraphOptions = false; + } + + if(updateJobsColors) + { + emit settings.jobColorsChanged(); + updateJobsColors = false; + } + + if(updateShiftGraphOptions) + { + emit settings.shiftGraphOptionsChanged(); + updateShiftGraphOptions = false; + } + + if(stopSetingsChanged) + { + emit settings.stopOptionsChanged(); + } +} + +void SettingsDialog::restoreDefaults() +{ + AppSettings.restoreDefaultSettings(); + loadSettings(); + updateJobsColors = true; + updateJobGraphOptions = true; + updateShiftGraphOptions = true; + saveSettings(); +} + +void SettingsDialog::closeEvent(QCloseEvent *e) +{ + int ret = QMessageBox::question(this, + tr("Settings"), + tr("Do you want to save settings?"), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if(ret == QMessageBox::Cancel) + { + e->ignore(); + return; + } + + if(ret == QMessageBox::Yes) + accept(); + + return QDialog::closeEvent(e); +} + +void SettingsDialog::onRestore() +{ + int ret = QMessageBox::question(this, + tr("Settings"), + tr("Do you want to restore default settings?"), + QMessageBox::Yes | QMessageBox::No); + if(ret == QMessageBox::Yes) + { + restoreDefaults(); + } +} + +void SettingsDialog::onJobColorChanged() +{ + updateJobsColors = true; +} + +void SettingsDialog::onJobGraphOptionsChanged() +{ + updateJobGraphOptions = true; +} + +void SettingsDialog::onShiftGraphOptionsChanged() +{ + updateShiftGraphOptions = true; +} diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h new file mode 100644 index 0000000..0996ad3 --- /dev/null +++ b/src/settings/settingsdialog.h @@ -0,0 +1,54 @@ +#ifndef SETTINGSDIALOG_H +#define SETTINGSDIALOG_H + +#include + +#include "utils/types.h" + +#include + +namespace Ui { +class SettingsDialog; +} + +class QSpinBox; +class ColorView; + +class SettingsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit SettingsDialog(QWidget *parent = nullptr); + ~SettingsDialog(); + +public slots: + void loadSettings(); + void saveSettings(); + void restoreDefaults(); + + void onRestore(); +protected: + void closeEvent(QCloseEvent *e); + +private slots: + void onJobColorChanged(); + void onJobGraphOptionsChanged(); + void onShiftGraphOptionsChanged(); + +private: + void setupLanguageBox(); + +private: + Ui::SettingsDialog *ui; + + bool updateJobsColors; + bool updateJobGraphOptions; + bool updateShiftGraphOptions; + + QSpinBox* m_timeSpinBoxArr[int(JobCategory::NCategories)]; + ColorView *m_colorViews[int(JobCategory::NCategories)]; + void setupJobColorsPage(); +}; + +#endif // SETTINGSDIALOG_H diff --git a/src/settings/settingsdialog.ui b/src/settings/settingsdialog.ui new file mode 100644 index 0000000..3325ce7 --- /dev/null +++ b/src/settings/settingsdialog.ui @@ -0,0 +1,919 @@ + + + SettingsDialog + + + + 0 + 0 + 600 + 500 + + + + + 300 + 200 + + + + + 16777215 + 16777215 + + + + + 600 + 400 + + + + Settings + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + 10 + 50 + false + + + + QTabWidget::North + + + QTabWidget::Rounded + + + 0 + + + true + + + + General + + + + + + + 100 + 0 + + + + Language + + + + + + + + 0 + 0 + + + + + 0 + 23 + + + + + + + + + 10 + 75 + true + + + + Language changes will be applied next time you start the application + + + + + + + + 16777215 + 16777215 + + + + Restore Default Settings + + + + + + + + Job Graph + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + 0 + 0 + 557 + 552 + + + + + + + + 0 + 0 + + + + Offsets + + + + + + Stations + + + + + + + 9999 + + + + + + + Hours + + + + + + + 9999 + + + + + + + Platforms + + + + + + + 9999 + + + + + + + Horizontal + + + + + + + 9999 + + + + + + + Vertical + + + + + + + 9999 + + + + + + + Hour Line Start + + + + + + + + + + + + + + 0 + 0 + + + + Line Width + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Job + + + + + + + + + + Hour + + + + + + + + + + Platforms + + + + + + + + + + + + + Colors + + + + + + Hour Line + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + + + + Hour Text + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + 30 + 30 + + + + + + + + Main Platform + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + + + + Depot Platform + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + + + + Station Text + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + + + + + + + + + + + + Stop + + + + + + + 0 + 0 + + + + Default Stop Duration + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + 0 + 0 + + + + Job Editor + + + false + + + + + + Auto insert transits between two stops. + + + + + + + Choose next line before editing newly added last stop. + + + + + + + Auto uncouple all rollingstock items at last stop. + + + + + + + Auto move uncoupled rollingstock pieces from last stop when adding a new one. + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Shift Graph + + + + + + + 0 + 0 + + + + Offsets + + + + + + Hours + + + + + + + 9999.989999999999782 + + + + + + + Horizontal + + + + + + + Vertical + + + + + + + + + + + + + Job + + + + + + + + + + Job Box + + + + + + + + + + Stations Labels + + + + + + + + + + Hide Same Stations + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Rollingstock + + + + + + + 0 + 0 + + + + Merge models + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + Remove source merged model by default + + + + + + + + + + + 0 + 0 + + + + Merge owners + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + Remove sourcemerged owner by default + + + + + + + + + + Open Document Spreadsheet Import + + + + + + First non-empty row + + + + + + + 1 + + + 9999 + + + + + + + Number column + + + + + + + Model column + + + + + + + 1 + + + 9999 + + + + + + + 1 + + + 9999 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Sheet Export + + + + + + Description + + + + + + Header + + + + + + + Footer + + + + + + + + + + + + + + + + Metadata + + + + + + Store meeting location and dates in sheet metadata + + + + + + + + + + Qt::Vertical + + + + 20 + 267 + + + + + + + + + Background Tasks + + + + + + Rollingstock Error Checker + + + + + + Check rollingstock when opening a file + + + + + + + Check rollingstock when a Job is edeited + + + + + + + + + + Qt::Vertical + + + + 498 + 215 + + + + + + + + + + + + + ColorView + QWidget +
utils/colorview.h
+ 1 +
+
+ + + + buttonBox + accepted() + SettingsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SettingsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/src/settings/type_utils.h b/src/settings/type_utils.h new file mode 100644 index 0000000..ba0a058 --- /dev/null +++ b/src/settings/type_utils.h @@ -0,0 +1,63 @@ +#ifndef REF_TYPE_H +#define REF_TYPE_H + +#include + +#include +#include +#include + +namespace utils { + +template struct const_ref_t +{ + typedef const T& Type; +}; + +//Don't pass arithmetic types as reference +template +struct const_ref_t::value>::type> { + typedef T Type; +}; + +//Specialize QColor and QLocale so they are more legible in the .ini settigs file, instead of Qt own's representation + +template +inline QVariant toVariant(typename const_ref_t::Type val) +{ + return QVariant(val); +} + +template <> +inline QVariant toVariant(typename const_ref_t::Type val) +{ + return QVariant(val.name(QColor::HexRgb)); +} + +template <> +inline QVariant toVariant(typename const_ref_t::Type val) +{ + return QVariant(val.name()); +} + +template +inline T fromVariant(const QVariant& v) +{ + return v.value(); +} + +template <> +inline QColor fromVariant(const QVariant& v) +{ + return QColor(v.toString()); +} + +template <> +inline QLocale fromVariant(const QVariant& v) +{ + return QLocale(v.toString()); +} + +} // namespace utils + +#endif // REF_TYPE_H diff --git a/src/shifts/CMakeLists.txt b/src/shifts/CMakeLists.txt new file mode 100644 index 0000000..e345668 --- /dev/null +++ b/src/shifts/CMakeLists.txt @@ -0,0 +1,20 @@ +add_subdirectory(shiftgraph) + +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + shifts/shiftcombomodel.h + shifts/shiftitem.h + shifts/shiftjobsmodel.h + shifts/shiftmanager.h + shifts/shiftresultevent.h + shifts/shiftsqlmodel.h + shifts/shiftviewer.h + + shifts/shiftcombomodel.cpp + shifts/shiftjobsmodel.cpp + shifts/shiftmanager.cpp + shifts/shiftresultevent.cpp + shifts/shiftsqlmodel.cpp + shifts/shiftviewer.cpp + PARENT_SCOPE +) diff --git a/src/shifts/shiftcombomodel.cpp b/src/shifts/shiftcombomodel.cpp new file mode 100644 index 0000000..ca63a34 --- /dev/null +++ b/src/shifts/shiftcombomodel.cpp @@ -0,0 +1,224 @@ +#include "shiftcombomodel.h" + +#include "app/session.h" + +#include + +#include +#include "shifts/shiftresultevent.h" + +ShiftComboModel::ShiftComboModel(database &db, QObject *parent) : + ISqlFKMatchModel(parent), + mDb(db), + totalCount(0), + isFetching(false) +{ +} + +bool ShiftComboModel::event(QEvent *e) +{ + if(e->type() == ShiftResultEvent::_Type) + { + ShiftResultEvent *ev = static_cast(e); + ev->setAccepted(true); + + isFetching = false; + if(totalCount < 0) + totalCount = -totalCount; //Now is positive + + QModelIndex firstIdx = index(0, 0); + QModelIndex lastIdx = index(size - 1, NCols - 1); + emit dataChanged(firstIdx, lastIdx); + + emit resultsReady(false); + + return true; + } + + return ISqlFKMatchModel::event(e); +} + +void ShiftComboModel::refreshData() +{ + int count = recalcRowCount(); + + if(count != totalCount) //Invalidate cache and reset model + { + beginResetModel(); + } + + if(count > MaxMatchItems) + { + size = MaxMatchItems + 2; //MaxItems + Empty + '...' + }else{ + size = count + 1; //Items + Empty + } + + if(count != totalCount) + { + endResetModel(); + } + + totalCount = -count; //Negative, clears cache +} + +void ShiftComboModel::clearCache() +{ + if(totalCount > 0) + totalCount = -totalCount; +} + +int ShiftComboModel::recalcRowCount() +{ + if(!mDb.db()) + return 0; + query q(mDb); + if(mQuery.isEmpty()) + { + q.prepare("SELECT COUNT(1) FROM jobshifts"); + }else{ + q.prepare("SELECT COUNT(1) FROM jobshifts WHERE name LIKE ?"); + q.bind(1, mQuery); + } + q.step(); + const int count = q.getRows().get(0); + return count; +} + +void ShiftComboModel::fetchRow() +{ + if(!mDb.db() || isFetching) + return; //Already fetching + + //TODO: use a custom QRunnable + /* + QMetaObject::invokeMethod(this, "internalFetch", Qt::QueuedConnection); + */ + internalFetch(); +} + +void ShiftComboModel::internalFetch() +{ + if(!mDb.db()) + { + //Error: database not open, empty result + ShiftResultEvent *ev = new ShiftResultEvent; + qApp->postEvent(this, ev); + return; + } + + query q(mDb); + + QByteArray sql = "SELECT id,name FROM jobshifts "; + + if(!mQuery.isEmpty()) + sql += "WHERE name LIKE ? "; + + sql += "ORDER BY name LIMIT " QT_STRINGIFY(MaxMatchItems); + + q.prepare(sql); + + if(!mQuery.isEmpty()) + q.bind(1, mQuery); + + auto it = q.begin(); + const auto end = q.end(); + + int i = 0; + for(; it != end && i < MaxMatchItems; ++it) + { + auto r = *it; + items[i].shiftId = r.get(0); + items[i].name = r.get(1); + i++; + } + + ShiftResultEvent *ev = new ShiftResultEvent; + qApp->postEvent(this, ev); +} + +QVariant ShiftComboModel::data(const QModelIndex &idx, int role) const +{ + if (!idx.isValid() || idx.row() >= size) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + { + if(totalCount < 0) + { + //Fetch above or below current cach + const_cast(this)->fetchRow(); + + //Temporarily return null + return QVariant(); + } + + if(isEmptyRow(idx.row())) + { + return ISqlFKMatchModel::tr("Empty"); + } + else if(isEllipsesRow(idx.row())) + { + return ellipsesString; + } + + if(isFetching) + break; //Don't acces data while fetching + + return items[idx.row()].name; + } + case Qt::FontRole: + { + if(isEmptyRow(idx.row())) + { + return boldFont(); + } + break; + } + } + + return QVariant(); +} + +void ShiftComboModel::autoSuggest(const QString &text) +{ + mQuery.clear(); + if(!text.isEmpty()) + { + mQuery.clear(); + mQuery.reserve(text.size() + 2); + mQuery.append('%'); + mQuery.append(text.toUtf8()); + mQuery.append('%'); + } + + refreshData(); +} + +QString ShiftComboModel::getName(db_id shiftId) const +{ + if(!mDb.db()) + return QString(); //Error: database not open + + QString shiftName; + query q_getShift(mDb, "SELECT name FROM jobshifts WHERE id=?"); + q_getShift.bind(1, shiftId); + if(q_getShift.step() == SQLITE_ROW) + { + auto r = q_getShift.getRows(); + shiftName = r.get(0); + } + return shiftName; +} + +db_id ShiftComboModel::getIdAtRow(int row) const +{ + return items[row].shiftId; +} + +QString ShiftComboModel::getNameAtRow(int row) const +{ + return items[row].name; +} diff --git a/src/shifts/shiftcombomodel.h b/src/shifts/shiftcombomodel.h new file mode 100644 index 0000000..e52a1af --- /dev/null +++ b/src/shifts/shiftcombomodel.h @@ -0,0 +1,61 @@ +#ifndef SHIFTCOMBOMODEL_H +#define SHIFTCOMBOMODEL_H + +#include "utils/sqldelegate/isqlfkmatchmodel.h" +#include + +#include "shifts/shiftitem.h" + +#include +using namespace sqlite3pp; + +class ShiftComboModel : public ISqlFKMatchModel +{ + Q_OBJECT + +public: + enum Columns + { + ShiftName = 0, + NCols + }; + + ShiftComboModel(database &db, QObject *parent = nullptr); + + // Basic functionality: + QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override; + + bool event(QEvent *e) override; + + // ISqlFKMatchModel: + void autoSuggest(const QString& text) override; + void refreshData() override; + void clearCache() override; + QString getName(db_id shiftId) const override; + + db_id getIdAtRow(int row) const override; + QString getNameAtRow(int row) const override; + +private: + int recalcRowCount(); + void fetchRow(); + Q_INVOKABLE void internalFetch(); + +private: + struct ShiftItem + { + db_id shiftId; + QString name; + char padding[4]; + }; + ShiftItem items[MaxMatchItems]; + + database &mDb; + + QString mQuery; + + int totalCount; + bool isFetching; +}; + +#endif // SHIFTCOMBOMODEL_H diff --git a/src/shifts/shiftgraph/CMakeLists.txt b/src/shifts/shiftgraph/CMakeLists.txt new file mode 100644 index 0000000..264e19b --- /dev/null +++ b/src/shifts/shiftgraph/CMakeLists.txt @@ -0,0 +1,17 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + shifts/shiftgraph/graphoptions.h + shifts/shiftgraph/jobchangeshiftdlg.h + shifts/shiftgraph/shiftgrapheditor.h + shifts/shiftgraph/shiftgraphholder.h + shifts/shiftgraph/shiftscene.h + shifts/shiftgraph/shiftgraphobj.h + + shifts/shiftgraph/graphoptions.cpp + shifts/shiftgraph/jobchangeshiftdlg.cpp + shifts/shiftgraph/shiftgrapheditor.cpp + shifts/shiftgraph/shiftgraphholder.cpp + shifts/shiftgraph/shiftscene.cpp + shifts/shiftgraph/shiftgraphobj.cpp + PARENT_SCOPE +) diff --git a/src/shifts/shiftgraph/graphoptions.cpp b/src/shifts/shiftgraph/graphoptions.cpp new file mode 100644 index 0000000..da3030f --- /dev/null +++ b/src/shifts/shiftgraph/graphoptions.cpp @@ -0,0 +1,51 @@ +#include "graphoptions.h" + +#include +#include + +#include +#include +#include + +#include + +GraphOptions::GraphOptions(QWidget *parent) : + QDialog(parent) +{ + view = new QListView; + + selectAllBut = new QPushButton(tr("Select All")); + selectNoneBut = new QPushButton(tr("Unselect All")); + + stationCheck = new QCheckBox(tr("Hide same station at job start")); + + QVBoxLayout *lay = new QVBoxLayout(this); + lay->addWidget(stationCheck); + + QGridLayout *viewLay = new QGridLayout; + viewLay->addWidget(selectAllBut, 0, 0); + viewLay->addWidget(selectNoneBut, 0, 1); + viewLay->addWidget(view, 1, 0, 1, 2); + + lay->addLayout(viewLay); + + QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Ok + | QDialogButtonBox::Cancel); + lay->addWidget(box); + + connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject); + + setMinimumSize(300, 200); + setWindowTitle(tr("Shifts Graph Options")); +} + +bool GraphOptions::hideSameStation() const +{ + return stationCheck->isChecked(); +} + +void GraphOptions::setHideSameStations(bool value) +{ + stationCheck->setChecked(value); +} diff --git a/src/shifts/shiftgraph/graphoptions.h b/src/shifts/shiftgraph/graphoptions.h new file mode 100644 index 0000000..63ae8bb --- /dev/null +++ b/src/shifts/shiftgraph/graphoptions.h @@ -0,0 +1,30 @@ +#ifndef GRAPHOPTIONS_H +#define GRAPHOPTIONS_H + +#include + +#include "utils/types.h" + +class QListView; +class QCheckBox; +class QPushButton; + +class GraphOptions : public QDialog +{ + Q_OBJECT +public: + explicit GraphOptions(QWidget *parent = nullptr); + + bool hideSameStation() const; + void setHideSameStations(bool value); + +private: + QListView *view; + + QPushButton *selectAllBut; + QPushButton *selectNoneBut; + + QCheckBox *stationCheck; +}; + +#endif // GRAPHOPTIONS_H diff --git a/src/shifts/shiftgraph/jobchangeshiftdlg.cpp b/src/shifts/shiftgraph/jobchangeshiftdlg.cpp new file mode 100644 index 0000000..114fb7b --- /dev/null +++ b/src/shifts/shiftgraph/jobchangeshiftdlg.cpp @@ -0,0 +1,132 @@ +#include "jobchangeshiftdlg.h" + +#include +#include +#include +#include +#include + +#include "utils/sqldelegate/customcompletionlineedit.h" +#include "shifts/shiftcombomodel.h" + +#include "jobs/jobeditor/shiftbusy/shiftbusydialog.h" +#include "jobs/jobeditor/shiftbusy/shiftbusymodel.h" + +#include "utils/jobcategorystrings.h" + +#include + +#include "app/session.h" + +JobChangeShiftDlg::JobChangeShiftDlg(sqlite3pp::database &db, QWidget *parent) : + QDialog(parent), + mJobId(0), + originalShiftId(0), + mDb(db) +{ + QVBoxLayout *lay = new QVBoxLayout(this); + + label = new QLabel; + lay->addWidget(label); + + ShiftComboModel *shiftComboModel = new ShiftComboModel(db, this); + shiftComboModel->refreshData(); + shiftCombo = new CustomCompletionLineEdit(shiftComboModel); + lay->addWidget(shiftCombo); + + QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + lay->addWidget(box); + + connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept); + + connect(Session, &MeetingSession::jobChanged, this, &JobChangeShiftDlg::onJobChanged); + + setMinimumSize(200, 100); +} + +void JobChangeShiftDlg::done(int ret) +{ + if(ret == QDialog::Accepted) + { + db_id shiftId = 0; + QString shiftName; + shiftCombo->getData(shiftId, shiftName); + if(shiftId && shiftId != originalShiftId) //FIXME: allow unset shift (choose empty shift, shiftId==0) + { + sqlite3pp::query q(mDb); + q.prepare("SELECT MIN(arrival) FROM stops WHERE jobId=?"); + q.bind(1, mJobId); + q.step(); + QTime start = q.getRows().get(0); + q.reset(); + + q.prepare("SELECT MAX(arrival) FROM stops WHERE jobId=?"); + q.bind(1, mJobId); + q.step(); + QTime end = q.getRows().get(0); + q.reset(); + + ShiftBusyModel model(mDb); + model.loadData(shiftId, mJobId, start, end); + if(model.hasConcurrentJobs()) + { + ShiftBusyDlg dlg(this); + dlg.setModel(&model); + dlg.exec(); + return; + } + + q.prepare("UPDATE jobs SET shiftId=? WHERE id=?"); + q.bind(1, shiftId); + q.bind(2, mJobId); + ret = q.step(); + if(ret != SQLITE_OK && ret != SQLITE_DONE) + { + //Error + QMessageBox::warning(this, tr("Shift Error"), + tr("Error while setting shift %1 to job %2.
" + "Msg: %3") + .arg(shiftName) + .arg(JobCategoryName::jobName(mJobId, mCategory)) + .arg(mDb.error_msg())); + return; + } + + emit Session->shiftJobsChanged(originalShiftId, mJobId); + emit Session->shiftJobsChanged(shiftId, mJobId); + } + } + + QDialog::done(ret); +} + +void JobChangeShiftDlg::onJobChanged(db_id jobId, db_id oldJobId) +{ + //Update id/category + if(mJobId == oldJobId) + setJob(jobId); +} + +void JobChangeShiftDlg::setJob(db_id jobId) +{ + query q_getJobInfo(mDb, "SELECT shiftId,category FROM jobs WHERE id=?"); + q_getJobInfo.bind(1, jobId); + if(q_getJobInfo.step() != SQLITE_ROW) + return; + auto j = q_getJobInfo.getRows(); + setJob(jobId, j.get(0), JobCategory(j.get(1))); +} + +void JobChangeShiftDlg::setJob(db_id jobId, db_id shiftId, JobCategory jobCat) +{ + mJobId = jobId; + originalShiftId = shiftId; + mCategory = jobCat; + + QString jobName = JobCategoryName::jobName(mJobId, mCategory); + + label->setText(tr("Change shift for job %1:").arg(jobName)); + shiftCombo->setData(shiftId); + setWindowTitle(jobName); +} diff --git a/src/shifts/shiftgraph/jobchangeshiftdlg.h b/src/shifts/shiftgraph/jobchangeshiftdlg.h new file mode 100644 index 0000000..e5ee215 --- /dev/null +++ b/src/shifts/shiftgraph/jobchangeshiftdlg.h @@ -0,0 +1,41 @@ +#ifndef JOBCHANGESHIFTDLG_H +#define JOBCHANGESHIFTDLG_H + +#include + +#include "utils/types.h" + +namespace sqlite3pp { +class database; +} + +class CustomCompletionLineEdit; +class QLabel; + +class JobChangeShiftDlg : public QDialog +{ + Q_OBJECT +public: + JobChangeShiftDlg(sqlite3pp::database &db, QWidget *parent = nullptr); + + void setJob(db_id jobId); + void setJob(db_id jobId, db_id shiftId, JobCategory jobCat); + +public slots: + virtual void done(int ret) override; + +private slots: + void onJobChanged(db_id jobId, db_id oldJobId); + +private: + db_id mJobId; + db_id originalShiftId; + JobCategory mCategory; + + CustomCompletionLineEdit *shiftCombo; + QLabel *label; + + sqlite3pp::database &mDb; +}; + +#endif // JOBCHANGESHIFTDLG_H diff --git a/src/shifts/shiftgraph/shiftgraph.pri b/src/shifts/shiftgraph/shiftgraph.pri new file mode 100644 index 0000000..9be4bb7 --- /dev/null +++ b/src/shifts/shiftgraph/shiftgraph.pri @@ -0,0 +1,17 @@ +#ShiftGraph subdirectory (project/shifts/shiftgraph/) + +HEADERS += \ + $$PWD/graphoptions.h \ + $$PWD/jobchangeshiftdlg.h \ + $$PWD/shiftgrapheditor.h \ + $$PWD/shiftgraphholder.h \ + $$PWD/shiftscene.h \ + $$PWD/shiftgraphobj.h + +SOURCES += \ + $$PWD/graphoptions.cpp \ + $$PWD/jobchangeshiftdlg.cpp \ + $$PWD/shiftgrapheditor.cpp \ + $$PWD/shiftgraphholder.cpp \ + $$PWD/shiftscene.cpp \ + $$PWD/shiftgraphobj.cpp diff --git a/src/shifts/shiftgraph/shiftgrapheditor.cpp b/src/shifts/shiftgraph/shiftgrapheditor.cpp new file mode 100644 index 0000000..3d8a587 --- /dev/null +++ b/src/shifts/shiftgraph/shiftgrapheditor.cpp @@ -0,0 +1,177 @@ +#include "shiftgrapheditor.h" + +#include + +#include +#include + +#include "graphoptions.h" + +#include "shiftgraphholder.h" +#include "shiftscene.h" + +#include "jobchangeshiftdlg.h" + +#include "app/session.h" + +#include +#include +#include "utils/file_format_names.h" + +#include + +#include +#include + +#include + +#include + +#include "info.h" + +ShiftGraphEditor::ShiftGraphEditor(QWidget *parent) : + QWidget(parent) +{ + toolBar = new QToolBar; + toolBar->addAction(tr("Save"), this, &ShiftGraphEditor::onSaveGraph); + toolBar->addAction(tr("Print"), this, &ShiftGraphEditor::onPrintGraph); + toolBar->addAction(tr("Options"), this, &ShiftGraphEditor::onShowOptions); + toolBar->addAction(tr("Refresh"), this, &ShiftGraphEditor::calculateGraph); + + view = new QGraphicsView; + view->setAlignment(Qt::AlignTop); + + graph = new ShiftGraphHolder(Session->m_Db, this); + graph->loadShifts(); + view->setScene(graph->scene()); + view->centerOn(0, 0); + connect(graph->scene(), &ShiftScene::jobDoubleClicked, this, &ShiftGraphEditor::showShiftMenuForJob); + + QVBoxLayout *lay = new QVBoxLayout(this); + lay->addWidget(toolBar); + lay->addWidget(view); + + setMinimumSize(300, 200); + setWindowTitle(tr("Shift Graph Editor")); +} + +ShiftGraphEditor::~ShiftGraphEditor() +{ + +} + +void ShiftGraphEditor::onSaveGraph() +{ + QFileDialog dlg(this, tr("Save Shift Graph")); + dlg.setFileMode(QFileDialog::AnyFile); + dlg.setAcceptMode(QFileDialog::AcceptSave); + dlg.setDirectory(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); + + QStringList filters; + filters << FileFormats::tr(FileFormats::svgFile); + filters << FileFormats::tr(FileFormats::pdfFile); + dlg.setNameFilters(filters); + + if(dlg.exec() != QDialog::Accepted) + return; + + QString fileName = dlg.selectedUrls().value(0).toLocalFile(); + + if(fileName.isEmpty()) + return; + + if(fileName.endsWith(QStringLiteral(".pdf"))) + { + exportPDF(fileName); + } + else if(fileName.endsWith(QStringLiteral(".svg"))) + { + exportSVG(fileName); + } + else if(dlg.selectedNameFilter().contains("pdf")) //TODO: needed? + { + exportPDF(fileName); + } + else + { + exportSVG(fileName); + } +} + +void ShiftGraphEditor::onPrintGraph() +{ + QPrinter printer; + + QPrintDialog dlg(&printer, this); + if(dlg.exec() == QDialog::Accepted) + { + print(&printer); + } +} + +void ShiftGraphEditor::exportSVG(const QString& fileName) +{ + QSvgGenerator svg; + svg.setTitle(QStringLiteral("Railway Shift")); + svg.setDescription(QStringLiteral("Generated by %1").arg(AppDisplayName)); + + QPainter painter; + + auto scene = graph->scene(); + QRectF r = scene->sceneRect(); + svg.setSize(r.size().toSize()); + svg.setViewBox(r); + + svg.setFileName(fileName); + + painter.begin(&svg); + + scene->render(&painter); +} + +void ShiftGraphEditor::exportPDF(const QString& fileName) +{ + QPrinter printer; + printer.setOutputFormat(QPrinter::PdfFormat); + printer.setOutputFileName(fileName); + printer.setCreator(AppDisplayName); + printer.setDocName(QStringLiteral("Railway Shift")); + + print(&printer); +} + +void ShiftGraphEditor::print(QPrinter *printer) +{ + QPainter painter(printer); + auto scene = graph->scene(); + scene->render(&painter); +} + +void ShiftGraphEditor::calculateGraph() +{ + graph->redrawGraph(); + refreshView(); +} + +void ShiftGraphEditor::refreshView() +{ + view->update(); +} + +void ShiftGraphEditor::onShowOptions() +{ + GraphOptions dlg(this); + dlg.setHideSameStations(graph->getHideSameStations()); + + if(dlg.exec() == QDialog::Rejected) + return; + + graph->setHideSameStations(dlg.hideSameStation()); +} + +void ShiftGraphEditor::showShiftMenuForJob(db_id jobId) +{ + JobChangeShiftDlg dlg(Session->m_Db, this); + dlg.setJob(jobId); + dlg.exec(); +} diff --git a/src/shifts/shiftgraph/shiftgrapheditor.h b/src/shifts/shiftgraph/shiftgrapheditor.h new file mode 100644 index 0000000..3f52ce0 --- /dev/null +++ b/src/shifts/shiftgraph/shiftgrapheditor.h @@ -0,0 +1,44 @@ +#ifndef SHIFTGRAPHEDITOR_H +#define SHIFTGRAPHEDITOR_H + +#include + +#include "utils/types.h" + +class QGraphicsView; +class QToolBar; + +class ShiftGraphHolder; + +class QPrinter; + +class ShiftGraphEditor : public QWidget +{ + Q_OBJECT +public: + ShiftGraphEditor(QWidget *parent = nullptr); + virtual ~ShiftGraphEditor(); + + void exportSVG(const QString &fileName); + void exportPDF(const QString &fileName); + void print(QPrinter *printer); + void updateJobColors(); + +public slots: + void onSaveGraph(); + void onPrintGraph(); + void onShowOptions(); + + void calculateGraph(); + + void refreshView(); + +private slots: + void showShiftMenuForJob(db_id jobId); +private: + QToolBar *toolBar; + QGraphicsView *view; + ShiftGraphHolder *graph; +}; + +#endif // SHIFTGRAPHEDITOR_H diff --git a/src/shifts/shiftgraph/shiftgraphholder.cpp b/src/shifts/shiftgraph/shiftgraphholder.cpp new file mode 100644 index 0000000..66a75b2 --- /dev/null +++ b/src/shifts/shiftgraph/shiftgraphholder.cpp @@ -0,0 +1,387 @@ +#include "shiftgraphholder.h" + +#include "shiftscene.h" + +#include +#include +#include "utils/model_roles.h" + +#include "shiftgraphobj.h" + +#include +#include "app/scopedebug.h" + +#include "app/session.h" +#include "utils/jobcategorystrings.h" +#include "lines/linestorage.h" + +ShiftGraphHolder::ShiftGraphHolder(database& db, QObject *parent) : + QObject(parent), + mScene(new ShiftScene(this)), + mDb(db), + q_selectShiftJobs(mDb, "SELECT jobs.id, jobs.category," + " s1.arrival, st1.id, st1.short_name, st1.name," + " s2.departure, st2.id, st2.short_name, st2.name" + " FROM jobs" + " JOIN stops s1 ON s1.id=jobs.firstStop" + " JOIN stops s2 ON s2.id=jobs.lastStop" + " JOIN stations st1 ON st1.id=s1.stationId" + " JOIN stations st2 ON st2.id=s2.stationId" + " WHERE jobs.shiftId=? ORDER BY s1.arrival ASC"), + hideSameStations(true) +{ + updateShiftGraphOptions(); + + connect(&AppSettings, &TrainTimetableSettings::jobColorsChanged, + this, &ShiftGraphHolder::updateJobColors); + + connect(&AppSettings, &TrainTimetableSettings::shiftGraphOptionsChanged, + this, &ShiftGraphHolder::updateShiftGraphOptions); + + connect(Session->mLineStorage, &LineStorage::stationNameChanged, this, &ShiftGraphHolder::stationNameChanged); + + connect(Session, &MeetingSession::shiftNameChanged, this, &ShiftGraphHolder::shiftNameChanged); + connect(Session, &MeetingSession::shiftRemoved, this, &ShiftGraphHolder::shiftRemoved); + connect(Session, &MeetingSession::shiftJobsChanged, this, &ShiftGraphHolder::shiftJobsChanged); +} + +ShiftGraphHolder::~ShiftGraphHolder() +{ + lookUp.clear(); + qDeleteAll(m_shifts); + m_shifts.clear(); +} + +ShiftScene *ShiftGraphHolder::scene() const +{ + return mScene; +} + +void ShiftGraphHolder::loadShifts() +{ + DEBUG_IMPORTANT_ENTRY; + + lookUp.clear(); + qDeleteAll(m_shifts); + m_shifts.clear(); + + query q_getShifts(mDb, "SELECT COUNT(1) FROM jobshifts"); + q_getShifts.step(); + int count = q_getShifts.getRows().get(0); + m_shifts.reserve(count); + + q_getShifts.prepare("SELECT id,name FROM jobshifts ORDER BY name"); + + QPen linePen(Qt::gray); + linePen.setWidth(4); + + QLineF backLine(0.0, 0.0, + 25.0 * hourOffset + horizOffset, 0.0); + + for(auto s : q_getShifts) //TODO: optimize + { + db_id shiftId = s.get(0); + QString name = tr("Shift %1").arg(s.get(1)); + + ShiftGraphObj *obj = new ShiftGraphObj(shiftId, mScene); + obj->setName(name, horizOffset); + + obj->line->setLine(backLine); + obj->line->setPen(linePen); + + drawShift(obj); + + lookUp.insert(shiftId, obj); + int i = 0; + for(; i < m_shifts.size(); i++) + { + if(name.compare(m_shifts.at(i)->getName()) < 0) + break; + } + + m_shifts.insert(i, obj); + + for(; i < m_shifts.size(); i++) + { + setObjPos(m_shifts[i], i); + } + } + + m_shifts.squeeze(); + + QRectF r = mScene->itemsBoundingRect(); + r.setTopLeft(QPointF(0, 0)); + mScene->setSceneRect(r); +} + +void ShiftGraphHolder::shiftNameChanged(db_id shiftId) //TODO: optimize +{ + DEBUG_IMPORTANT_ENTRY; + auto it = lookUp.constFind(shiftId); + if(it == lookUp.constEnd()) + return; + + const QString newName = tr("Shift %1").arg(getShiftName(shiftId)); + + ShiftGraphObj *obj = it.value(); + int oldPos = obj->pos; + + int res = newName.compare(obj->getName()); + if(res == 0) + { + return; //They are equal + } + else if(res < 0) + { + int i = 0; + for(; i < oldPos; i++) + { + if(newName.compare(m_shifts.at(i)->getName()) < 0) + { + break; + } + } + + //m_shifts.removeAt(oldPos); + //m_shifts.insert(i, obj); + m_shifts.move(oldPos, i); //Small Optimization + + for(; i <= oldPos; i++) + { + setObjPos(m_shifts[i], i); + } + } + else + { + //m_shifts.removeAt(oldPos); + + int i = oldPos + 1; + for(; i < m_shifts.size(); i++) + { + if(newName.compare(m_shifts.at(i)->getName()) < 0) + { + break; + } + + setObjPos(m_shifts[i], i - 1); + } + + //m_shifts.insert(i, obj); + m_shifts.move(oldPos, i - 1); //Small Optimization + + setObjPos(obj, i - 1); + } + + obj->setName(newName, horizOffset); +} + +void ShiftGraphHolder::shiftRemoved(db_id shiftId) +{ + DEBUG_IMPORTANT_ENTRY; + auto it = lookUp.constFind(shiftId); + if(it == lookUp.constEnd()) + return; + + ShiftGraphObj *obj = it.value(); + int pos = obj->pos; + + lookUp.erase(it); + m_shifts.removeAt(pos); + for(int i = pos; i < m_shifts.size(); i++) + { + setObjPos(m_shifts[i], i); + } + + delete obj; +} + +void ShiftGraphHolder::shiftJobsChanged(db_id shiftId) +{ + DEBUG_ENTRY; + auto it = lookUp.constFind(shiftId); + if(it == lookUp.constEnd()) + return; + + drawShift(it.value()); +} + +void ShiftGraphHolder::stationNameChanged(db_id /*stId*/) +{ + //TODO: update only labels + redrawGraph(); +} + +void ShiftGraphHolder::drawShift(ShiftGraphObj *obj) +{ + DEBUG_IMPORTANT_ENTRY; + + db_id lastSt = 0; + int pos = 0; + + obj->clearJobs(); + obj->centerName(horizOffset); + + QPen p; + p.setWidth(5); + + q_selectShiftJobs.bind(1, obj->shiftId); + for(auto job : q_selectShiftJobs) + { + db_id jobId = job.get(0); + JobCategory cat = JobCategory(job.get(1)); + + QTime arr = job.get(2); + db_id st1 = job.get(3); + QString st1_name = job.get(4); + + //If 'Short Name' is empty fallback to 'Full Name' + if(st1_name.isEmpty()) + st1_name = job.get(5); + + QTime dep = job.get(6); + db_id st2 = job.get(7); + QString st2_name = job.get(8); + + //If 'Short Name' is empty fallback to 'Full Name' + if(st2_name.isEmpty()) + st2_name = job.get(9); + + const qreal y = obj->pos * jobOffset + vertOffset; + + ShiftGraphObj::JobGraph g; + g.pos = pos; + + QLineF l(jobPos(arr), 0, jobPos(dep), 0); + + p.setColor(Session->colorForCat(cat)); + g.line = mScene->addLine(l, p); + g.line->setY(y + jobOffset / 2); + g.line->setData(JOB_ID_ROLE, jobId); + + g.name = mScene->addSimpleText(JobCategoryName::jobName(jobId, cat)); + QRectF nameRect = g.name->boundingRect(); + nameRect.moveCenter(l.center()); + g.name->setPos(nameRect.left(), y); + + if(!hideSameStations || lastSt != st1) + { + g.st1_label = mScene->addSimpleText(st1_name); + g.st1_label->setPos(l.x1(), y + stationNameOffset + jobBoxOffset); + } + + g.st2_label = mScene->addSimpleText(st2_name); + g.st2_label->setPos(l.x2(), y + stationNameOffset + jobBoxOffset); + + lastSt = st2; + + auto iter = obj->jobs.insert(jobId, g); + obj->vec.append(iter); + + } + q_selectShiftJobs.reset(); + + obj->jobs.squeeze(); + obj->vec.squeeze(); +} + +bool ShiftGraphHolder::getHideSameStations() const +{ + return hideSameStations; +} + +void ShiftGraphHolder::setHideSameStations(bool value) +{ + hideSameStations = value; +} + +void ShiftGraphHolder::redrawGraph() +{ + loadShifts(); //Reload + + for(int i = 0; i < m_shifts.size(); i++) + { + setObjPos(m_shifts[i], i); + drawShift(m_shifts[i]); + } + + mScene->invalidate(mScene->sceneRect()); +} + +void ShiftGraphHolder::updateJobColors() +{ + QPen p; + p.setWidth(5); + + //TODO: maybe store JobCategory inside JobGraph + query q_getJobCat(mDb, "SELECT category FROM jobs WHERE id=?"); + + for(ShiftGraphObj *obj : m_shifts) + { + for(auto it : obj->vec) + { + db_id jobId = it.key(); + ShiftGraphObj::JobGraph& g = it.value(); + + q_getJobCat.bind(1, jobId); + if(q_getJobCat.step() != SQLITE_ROW) + { + //Error: job does not exist! + } + JobCategory cat = JobCategory(q_getJobCat.getRows().get(0)); + + p.setColor(Session->colorForCat(cat)); + + g.line->setPen(p); + } + } +} + +QString ShiftGraphHolder::getShiftName(db_id shiftId) const +{ + query q_getShiftName(mDb, "SELECT name FROM jobshifts WHERE id=?"); //TODO: same query as above, make string constant + QString name; + q_getShiftName.bind(1, shiftId); + if(q_getShiftName.step() == SQLITE_ROW) + { + name = q_getShiftName.getRows().get(0); + }else{ + name = "Error"; + } + q_getShiftName.reset(); + + return name; +} + +void ShiftGraphHolder::updateShiftGraphOptions() +{ + mScene->hourOffset = hourOffset = AppSettings.getShiftHourOffset(); + mScene->horizOffset = horizOffset = AppSettings.getShiftHorizOffset(); + mScene->vertOffset = vertOffset = AppSettings.getShiftVertOffset(); + + jobOffset = AppSettings.getShiftJobOffset(); + jobBoxOffset = AppSettings.getShiftJobBoxOffset(); + stationNameOffset = AppSettings.getShiftStationOffset(); + hideSameStations = AppSettings.getShiftHideSameStations(); + + redrawGraph(); +} + +void ShiftGraphHolder::setObjPos(ShiftGraphObj *o, int pos) +{ + o->pos = pos; + + const qreal y = pos * jobOffset + vertOffset; + o->text->setY(y + jobBoxOffset); + o->line->setY(y + jobOffset / 2 + jobBoxOffset); + + for(ShiftGraphObj::JobGraph& g : o->jobs) + { + g.line->setY(y + jobOffset / 2); + g.name->setY(y); + + if(g.st1_label) + g.st1_label->setY(y + stationNameOffset + jobBoxOffset); + + g.st2_label->setY(y + stationNameOffset + jobBoxOffset); + } +} diff --git a/src/shifts/shiftgraph/shiftgraphholder.h b/src/shifts/shiftgraph/shiftgraphholder.h new file mode 100644 index 0000000..5c9edbf --- /dev/null +++ b/src/shifts/shiftgraph/shiftgraphholder.h @@ -0,0 +1,85 @@ +#ifndef SHIFTGRAPHHOLDER_H +#define SHIFTGRAPHHOLDER_H + +#include +#include +#include + +#include "utils/types.h" + +#include + +using namespace sqlite3pp; + +class QGraphicsScene; +class ShiftScene; + +class ShiftGraphObj; + +constexpr qreal MSEC_PER_HOUR = 1000 * 60 * 60; + +//FIXME: make incremental load +class ShiftGraphHolder : public QObject +{ + Q_OBJECT +public: + ShiftGraphHolder(database& db, QObject *parent = nullptr); + ~ShiftGraphHolder(); + + ShiftScene *scene() const; + + void loadShifts(); + + bool getHideSameStations() const; + void setHideSameStations(bool value); + + + QString getShiftName(db_id shiftId) const; + +public slots: + void shiftNameChanged(db_id shiftId); + void shiftRemoved(db_id shiftId); + + void shiftJobsChanged(db_id shiftId); + + void stationNameChanged(db_id stId); + + void redrawGraph(); + + void updateJobColors(); + void updateShiftGraphOptions(); + +private: + void drawShift(ShiftGraphObj *obj); + + void setObjPos(ShiftGraphObj *o, int pos); + + + inline qreal jobPos(const QTime& t) + { + return t.msecsSinceStartOfDay() / MSEC_PER_HOUR * hourOffset + horizOffset; + } + +private: + ShiftScene *mScene; + + QHash lookUp; + QVector m_shifts; + + database& mDb; + + query q_selectShiftJobs; //FIXME: on demand + + qreal hourOffset = 150; + qreal jobOffset = 50; + + qreal horizOffset = 50; + qreal vertOffset = 20; + + qreal jobBoxOffset = 20; + qreal stationNameOffset = 5; + + bool hideSameStations; +}; + +#endif // SHIFTGRAPHHOLDER_H diff --git a/src/shifts/shiftgraph/shiftgraphobj.cpp b/src/shifts/shiftgraph/shiftgraphobj.cpp new file mode 100644 index 0000000..555fc88 --- /dev/null +++ b/src/shifts/shiftgraph/shiftgraphobj.cpp @@ -0,0 +1,84 @@ +#include "shiftgraphobj.h" + +#include +#include +#include + +ShiftGraphObj::ShiftGraphObj(db_id id, QGraphicsScene *scene) : + shiftId(id), + pos(0) +{ + text = scene->addSimpleText(QString()); + text->setBrush(Qt::red); + QFont f; + //f.setBold(true); + f.setPointSize(12); + text->setFont(f); + line = scene->addLine(QLineF()); +} + +ShiftGraphObj::~ShiftGraphObj() +{ + delete text; + delete line; + + clearJobs(); +} + +QString ShiftGraphObj::getName() const +{ + return text->text(); +} + +void ShiftGraphObj::setName(const QString &value, double horizOffset) +{ + text->setText(value); + centerName(horizOffset); +} + +inline void cleanUp(ShiftGraphObj::JobGraph& g) +{ + delete g.line; + delete g.name; + + if(g.st1_label) + delete g.st1_label; //Can be NULL if HideSameStations is set (default) + + delete g.st2_label; +} + +void ShiftGraphObj::clearJobs() +{ + for(JobGraph& g : jobs) + { + cleanUp(g); + } + jobs.clear(); + vec.clear(); +} + +void ShiftGraphObj::removeJob(db_id jobId) +{ + auto it = jobs.find(jobId); + if(it == jobs.end()) + return; + JobGraph& g = it.value(); + + cleanUp(g); + + int jobPos = g.pos; + + vec.removeAt(jobPos); + for(int i = jobPos; i < vec.size(); i++) + { + vec[i].value().pos--; + } +} + +void ShiftGraphObj::centerName(double horizOffset) +{ + double y = text->y(); + QRectF r = text->boundingRect(); + r.moveCenter(QPointF(horizOffset / 2, r.center().y())); + text->setPos(r.left(), y); +} diff --git a/src/shifts/shiftgraph/shiftgraphobj.h b/src/shifts/shiftgraph/shiftgraphobj.h new file mode 100644 index 0000000..ebd4b66 --- /dev/null +++ b/src/shifts/shiftgraph/shiftgraphobj.h @@ -0,0 +1,50 @@ +#ifndef SHIFTOBJ_H +#define SHIFTOBJ_H + +#include +#include +#include + +#include "utils/types.h" + +class QGraphicsScene; +class QGraphicsSimpleTextItem; +class QGraphicsLineItem; + +class ShiftGraphObj +{ +public: + ShiftGraphObj(db_id id, QGraphicsScene *scene); + ~ShiftGraphObj(); + + QString getName() const; + void setName(const QString &value, double horizOffset); + + void clearJobs(); + + void removeJob(db_id jobId); + + void centerName(double horizOffset); + + db_id shiftId; + + QGraphicsSimpleTextItem *text; + QGraphicsLineItem *line; + + typedef struct JobGraph_ + { + QGraphicsLineItem *line = nullptr; + QGraphicsSimpleTextItem *name = nullptr; + QGraphicsSimpleTextItem *st1_label = nullptr; + QGraphicsSimpleTextItem *st2_label = nullptr; + int pos; + } JobGraph; + + typedef QHash Jobs; + Jobs jobs; + QVector vec; + + int pos; +}; + +#endif // SHIFTOBJ_H diff --git a/src/shifts/shiftgraph/shiftscene.cpp b/src/shifts/shiftgraph/shiftscene.cpp new file mode 100644 index 0000000..afb0119 --- /dev/null +++ b/src/shifts/shiftgraph/shiftscene.cpp @@ -0,0 +1,92 @@ +#include "shiftscene.h" + +#include + +#include + +#include + +#include +#include "utils/model_roles.h" + +ShiftScene::ShiftScene(QObject *parent) : + QGraphicsScene(parent) +{ + linePen.setWidth(4); + linePen.setColor(Qt::gray); +} + +void ShiftScene::drawBackground(QPainter *painter, const QRectF& rect) +{ + const qreal x1 = rect.left(); + const qreal x2 = rect.right(); + const qreal t = qMax(rect.top(), vertOffset); + const qreal b = rect.bottom(); + + int first = qFloor((x1 - horizOffset) / hourOffset); + qreal firstX = first * hourOffset + horizOffset; + + if(first < 0.0) + { + first = 0.0; + firstX = horizOffset; + } + + int last = qCeil((x2 - horizOffset) / hourOffset); + + if(first > last) + return; + + if(rect.top() < vertOffset * 3) + { + painter->setPen(Qt::blue); + painter->setFont(QFont("ARIAL", 10, QFont::Bold)); + + const QString fmt = QStringLiteral("%1:00"); + double huorX = firstX; + for(int i = first; i < last; i++) + { + painter->drawText(QPointF(huorX, 15), fmt.arg(i)); + huorX += hourOffset; + } + } + + if(t < b) + { + size_t n = size_t(last - first + 1); + QLineF *arr = new QLineF[n]; + double lineX = firstX; + for(std::size_t i = 0; i < n; i++) + { + arr[i] = QLineF(lineX, t, lineX, b); + lineX += hourOffset; + } + + painter->setPen(linePen); + painter->drawLines(arr, int(n)); + delete [] arr; + } +} + +void ShiftScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *e) +{ + if(e->button() == Qt::MouseButton::LeftButton) + { + QList list = items(e->scenePos()); + for(QGraphicsItem *i : list) + { + QVariant v = i->data(JOB_ID_ROLE); + bool ok = false; + db_id jobId = v.toLongLong(&ok); + if(ok && jobId != 0) + { + emit jobDoubleClicked(jobId); + + e->setAccepted(true); + return; + } + } + } + + QGraphicsScene::mouseDoubleClickEvent(e); +} diff --git a/src/shifts/shiftgraph/shiftscene.h b/src/shifts/shiftgraph/shiftscene.h new file mode 100644 index 0000000..a289054 --- /dev/null +++ b/src/shifts/shiftgraph/shiftscene.h @@ -0,0 +1,28 @@ +#ifndef SHIFTSCENE_H +#define SHIFTSCENE_H + +#include + +#include "utils/types.h" + +class ShiftScene : public QGraphicsScene +{ + Q_OBJECT +public: + explicit ShiftScene(QObject *parent = nullptr); + + qreal hourOffset; + qreal horizOffset; + qreal vertOffset; + QPen linePen; + +signals: + void jobDoubleClicked(db_id jobId); + +protected: + virtual void drawBackground(QPainter *painter, const QRectF &rect) override; + + virtual void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *e) override; +}; + +#endif // SHIFTSCENE_H diff --git a/src/shifts/shiftitem.h b/src/shifts/shiftitem.h new file mode 100644 index 0000000..2bb6949 --- /dev/null +++ b/src/shifts/shiftitem.h @@ -0,0 +1,13 @@ +#ifndef SHIFTITEM_H +#define SHIFTITEM_H + +#include +#include "utils/types.h" + +typedef struct +{ + db_id shiftId; + QString shiftName; +} ShiftItem; + +#endif // SHIFTITEM_H diff --git a/src/shifts/shiftjobsmodel.cpp b/src/shifts/shiftjobsmodel.cpp new file mode 100644 index 0000000..62afe25 --- /dev/null +++ b/src/shifts/shiftjobsmodel.cpp @@ -0,0 +1,115 @@ +#include "shiftjobsmodel.h" + +#include "utils/jobcategorystrings.h" + +#include +using namespace sqlite3pp; + +ShiftJobsModel::ShiftJobsModel(sqlite3pp::database &db, QObject *parent) : + QAbstractTableModel(parent), + mDb(db) +{ +} + +QVariant ShiftJobsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch (section) + { + case JobName: + return tr("Job"); + case Departure: + return tr("Departure"); + case Origin: + return tr("Origin"); + case Arrival: + return tr("Arrival"); + case Destination: + return tr("Destination"); + default: + break; + } + } + return QAbstractTableModel::headerData(section, orientation, role); + } + +int ShiftJobsModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_data.size(); +} + +int ShiftJobsModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant ShiftJobsModel::data(const QModelIndex &idx, int role) const +{ + if (!idx.isValid() || idx.row() >= m_data.size() || idx.column() >= NCols || role != Qt::DisplayRole) + return QVariant(); + + const ShiftJobItem& item = m_data.at(idx.row()); + + switch (idx.column()) + { + case JobName: + return JobCategoryName::jobName(item.jobId, item.cat); + case Departure: + return item.start; + case Origin: + return item.originStName; + case Arrival: + return item.end; + case Destination: + return item.desinationStName; + } + + return QVariant(); +} + +void ShiftJobsModel::loadShiftJobs(db_id shiftId) +{ + beginResetModel(); + + m_data.clear(); + + query q(mDb, "SELECT jobs.id," + " jobs.category," + " s1.arrival, s1.stationId," + " s2.departure, s2.stationId," + " st1.name,st2.name" + " FROM jobs" + " JOIN stops s1 ON s1.id=jobs.firstStop" + " JOIN stops s2 ON s2.id=jobs.lastStop" + " JOIN stations st1 ON st1.id=s1.stationId" + " JOIN stations st2 ON st2.id=s2.stationId" + " WHERE jobs.shiftId=? ORDER BY s1.arrival ASC"); + + q.bind(1, shiftId); + + for(auto r : q) + { + ShiftJobItem item; + item.jobId = r.get(0); + item.cat = JobCategory(r.get(1)); + item.start = r.get(2); + item.originStId = r.get(3); + item.end = r.get(4); + item.destinationStId = r.get(5); + item.originStName = r.get(6); + item.desinationStName = r.get(7); + m_data.append(item); + } + + m_data.squeeze(); + + endResetModel(); +} + +db_id ShiftJobsModel::getJobAt(int row) +{ + if(row < m_data.size()) + return m_data.at(row).jobId; + return 0; +} diff --git a/src/shifts/shiftjobsmodel.h b/src/shifts/shiftjobsmodel.h new file mode 100644 index 0000000..ea2e18c --- /dev/null +++ b/src/shifts/shiftjobsmodel.h @@ -0,0 +1,64 @@ +#ifndef SHIFTJOBSMODEL_H +#define SHIFTJOBSMODEL_H + +#include + +#include + +#include + +#include "utils/types.h" + +namespace sqlite3pp { +class database; +} + +typedef struct ShiftJobItem_ +{ + db_id jobId; + db_id originStId; + db_id destinationStId; + QTime start; + QTime end; + JobCategory cat; + QString originStName; + QString desinationStName; +} ShiftJobItem; + +class ShiftJobsModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + enum Columns + { + JobName, + Departure, + Origin, + Arrival, + Destination, + NCols + }; + + ShiftJobsModel(sqlite3pp::database &db, QObject *parent = nullptr); + + // 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; + + void loadShiftJobs(db_id shiftId); + + db_id getJobAt(int row); + +private: + sqlite3pp::database& mDb; + + QVector m_data; +}; + +#endif // SHIFTJOBSMODEL_H diff --git a/src/shifts/shiftmanager.cpp b/src/shifts/shiftmanager.cpp new file mode 100644 index 0000000..dfd8adc --- /dev/null +++ b/src/shifts/shiftmanager.cpp @@ -0,0 +1,178 @@ +#include "shiftmanager.h" + +#include "shiftsqlmodel.h" +#include "utils/sqldelegate/modelpageswitcher.h" + +#include "app/session.h" +#include "viewmanager/viewmanager.h" + +#include "app/scopedebug.h" + +#include + +#include +#include +#include +#include + +#include +#include + +#include "odt_export/shiftsheetexport.h" + +#include "utils/file_format_names.h" + +ShiftManager::ShiftManager(QWidget *parent) : + QWidget(parent), + m_readOnly(false), + edited(false) +{ + QVBoxLayout *l = new QVBoxLayout(this); + + toolBar = new QToolBar(this); + l->addWidget(toolBar); + + view = new QTableView(this); + l->addWidget(view); + + auto ps = new ModelPageSwitcher(false, this); + l->addWidget(ps); + + QFont f = view->font(); + f.setPointSize(12); + view->setFont(f); + + model = new ShiftSQLModel(Session->m_Db, this); + model->refreshData(); + ps->setModel(model); + view->setModel(model); + + act_New = toolBar->addAction(tr("New"), this, &ShiftManager::onNewShift); + act_Remove = toolBar->addAction(tr("Remove"), this, &ShiftManager::onRemoveShift); + act_displayShift = toolBar->addAction(tr("View Shift"), this, &ShiftManager::onViewShift); + act_Sheet = toolBar->addAction(tr("Sheet"), this, &ShiftManager::onSaveSheet); + act_Graph = toolBar->addAction(tr("Graph"), this, &ShiftManager::displayGraph); + + actionGroup = new QActionGroup(this); + actionGroup->addAction(act_New); + actionGroup->addAction(act_Remove); + + setReadOnly(false); + + setWindowTitle(tr("Shift Manager")); + setMinimumSize(300, 200); +} + +void ShiftManager::showEvent(QShowEvent *e) +{ + model->refreshData(); + QWidget::showEvent(e); +} + +void ShiftManager::setReadOnly(bool readOnly) +{ + if(m_readOnly == readOnly) + return; + + m_readOnly = readOnly; + + actionGroup->setEnabled(!m_readOnly); + + if(m_readOnly) + { + view->setEditTriggers(QTableView::NoEditTriggers); + } + else + { + view->setEditTriggers(QTableView::DoubleClicked); + } +} + +void ShiftManager::onNewShift() +{ + DEBUG_ENTRY; + if(m_readOnly) + return; + + int row = 0; + if(!model->addShift(&row) || row == -1) + { + QMessageBox::warning(this, + tr("Error Adding Shift"), + tr("An error occurred while adding a new shift:\n%1") + .arg(Session->m_Db.error_msg())); + return; + } + + QModelIndex index = model->index(row, ShiftSQLModel::ShiftName); + view->setCurrentIndex(index); + view->scrollTo(index); + view->edit(index); //FIXME: item is not yet fetched so editing fails, maybe queue edit? +} + +void ShiftManager::onRemoveShift() +{ + DEBUG_ENTRY; + if(m_readOnly || !view->selectionModel()->hasSelection()) + return; + + QModelIndex idx = view->currentIndex(); + model->removeShiftAt(idx.row()); +} + +void ShiftManager::onViewShift() +{ + DEBUG_ENTRY; + if(!view->selectionModel()->hasSelection()) + return; + + db_id shiftId = model->shiftAtRow(view->currentIndex().row()); + if(!shiftId) + return; + + qDebug() << "Display Shift:" << shiftId; + + Session->getViewManager()->requestShiftViewer(shiftId); +} + +void ShiftManager::displayGraph() +{ + Session->getViewManager()->requestShiftGraphEditor(); +} + +void ShiftManager::onSaveSheet() +{ + DEBUG_ENTRY; + if(!view->selectionModel()->hasSelection()) + return; + + QModelIndex idx = view->currentIndex(); + db_id shiftId = model->shiftAtRow(idx.row()); + if(!shiftId) + return; + + QString shiftName = model->shiftNameAtRow(idx.row()); + qDebug() << "Printing Shift:" << shiftId; + + QFileDialog dlg(this, tr("Save Shift Sheet")); + dlg.setFileMode(QFileDialog::AnyFile); + dlg.setAcceptMode(QFileDialog::AcceptSave); + dlg.setDirectory(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); + dlg.selectFile(tr("shift_%1.odt").arg(shiftName)); + + 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; + + ShiftSheetExport w(shiftId); + w.write(); + w.save(fileName); +} diff --git a/src/shifts/shiftmanager.h b/src/shifts/shiftmanager.h new file mode 100644 index 0000000..59a6486 --- /dev/null +++ b/src/shifts/shiftmanager.h @@ -0,0 +1,54 @@ +#ifndef SHIFTMANAGER_H +#define SHIFTMANAGER_H + +#include + +#include "utils/types.h" + +#include + +using namespace sqlite3pp; + +class QToolBar; +class QTableView; +class ShiftSQLModel; + +class QActionGroup; + +class ShiftManager : public QWidget +{ + Q_OBJECT +public: + explicit ShiftManager(QWidget *parent = nullptr); + + void setReadOnly(bool readOnly = true); + +protected: + void showEvent(QShowEvent *e) override; + +private slots: + void onNewShift(); + void onRemoveShift(); + void onViewShift(); + void onSaveSheet(); + void displayGraph(); + +private: + QToolBar *toolBar; + QTableView *view; + + ShiftSQLModel *model; + + QAction *act_New; + QAction *act_Remove; + QAction *act_displayShift; + QAction *act_Sheet; + QAction *act_Graph; + + QActionGroup *actionGroup; + + bool m_readOnly; + bool edited; +}; + +#endif // SHIFTMANAGER_H diff --git a/src/shifts/shiftresultevent.cpp b/src/shifts/shiftresultevent.cpp new file mode 100644 index 0000000..4dd0184 --- /dev/null +++ b/src/shifts/shiftresultevent.cpp @@ -0,0 +1,7 @@ +#include "shiftresultevent.h" + +ShiftResultEvent::ShiftResultEvent() : + QEvent(_Type) +{ + +} diff --git a/src/shifts/shiftresultevent.h b/src/shifts/shiftresultevent.h new file mode 100644 index 0000000..a8f3e16 --- /dev/null +++ b/src/shifts/shiftresultevent.h @@ -0,0 +1,21 @@ +#ifndef SHIFTRESULTEVENT_H +#define SHIFTRESULTEVENT_H + +#include +#include "utils/worker_event_types.h" + +#include +#include "shiftitem.h" + +class ShiftResultEvent : public QEvent +{ +public: + static const Type _Type = Type(CustomEvents::ShiftWorkerResult); + + ShiftResultEvent(); + + QVector items; + int firstRow; +}; + +#endif // SHIFTRESULTEVENT_H diff --git a/src/shifts/shiftsqlmodel.cpp b/src/shifts/shiftsqlmodel.cpp new file mode 100644 index 0000000..bad5e8c --- /dev/null +++ b/src/shifts/shiftsqlmodel.cpp @@ -0,0 +1,497 @@ +#include "shiftsqlmodel.h" + +#include "app/session.h" + +#include + +#include +#include "shiftresultevent.h" + +ShiftSQLModel::ShiftSQLModel(database &db, QObject *parent) : + IPagedItemModel(500, db, parent), + cacheFirstRow(0), + firstPendingRow(-BatchSize) +{ +} + +bool ShiftSQLModel::event(QEvent *e) +{ + if(e->type() == ShiftResultEvent::_Type) + { + ShiftResultEvent *ev = static_cast(e); + ev->setAccepted(true); + + handleResult(ev->items, ev->firstRow); + + return true; + } + + return QAbstractTableModel::event(e); +} + +QVariant ShiftSQLModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch (section) + { + case ShiftName: + return tr("Shift Name"); + } + } + return QAbstractTableModel::headerData(section, orientation, role); +} + +int ShiftSQLModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : curItemCount; +} + +int ShiftSQLModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant ShiftSQLModel::data(const QModelIndex &idx, int role) const +{ + const int row = idx.row(); + if (!idx.isValid() || row >= curItemCount || idx.column() >= NCols) + return QVariant(); + + if(row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + { + //Fetch above or below current cach + const_cast(this)->fetchRow(row); + + //Temporarily return null + return role == Qt::DisplayRole ? QVariant("...") : QVariant(); + } + + switch (role) + { + case Qt::DisplayRole: + case Qt::EditRole: + { + const ShiftItem& item = cache.at(idx.row() - cacheFirstRow); + + switch (idx.column()) + { + case ShiftName: + return item.shiftName; + } + } + } + + return QVariant(); +} + +bool ShiftSQLModel::setData(const QModelIndex &idx, const QVariant &value, int role) +{ + const int row = idx.row(); + if(!idx.isValid() || row >= curItemCount || idx.column() >= NCols || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + ShiftItem &item = cache[row - cacheFirstRow]; + + switch (role) + { + case Qt::EditRole: + { + switch (idx.column()) + { + case ShiftName: + { + QString newName = value.toString().simplified(); + if(item.shiftName == newName) + return false; //No change + + command set_name(mDb, "UPDATE jobshifts SET name=? WHERE id=?"); + + if(newName.isEmpty()) + set_name.bind(1); //Bind NULL + else + set_name.bind(1, newName); + set_name.bind(2, item.shiftId); + int ret = set_name.execute(); + if(ret == SQLITE_CONSTRAINT_UNIQUE) + { + emit modelError(tr("There is already another job shift with same name: %1").arg(newName)); + return false; + } + else if(ret != SQLITE_OK) + { + return false; + } + + item.shiftName = newName; + emit Session->shiftNameChanged(item.shiftId); + + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + + break; + } + } + } + } + + emit dataChanged(idx, idx); + return true; +} + +Qt::ItemFlags ShiftSQLModel::flags(const QModelIndex &idx) const +{ + if (!idx.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags f = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemNeverHasChildren; + if(idx.row() < cacheFirstRow || idx.row() >= cacheFirstRow + cache.size()) + return f; + + return f | Qt::ItemIsEditable; +} + +void ShiftSQLModel::clearCache() +{ + cache.clear(); + cache.squeeze(); + cacheFirstRow = 0; +} + +void ShiftSQLModel::refreshData() +{ + if(!mDb.db()) + return; + + query q(mDb); + if(mQuery.size() <= 2) // '%%' -> '%%' + { + q.prepare("SELECT COUNT(1) FROM jobshifts"); + }else{ + q.prepare("SELECT COUNT(1) FROM jobshifts WHERE name LIKE ?"); + q.bind(1, mQuery); + } + q.step(); + const int count = q.getRows().get(0); + + if(count != totalItemsCount) //Invalidate cache and reset model + { + 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 ShiftSQLModel::setSortingColumn(int /*col*/) +{ + //Sort only by name +} + +void ShiftSQLModel::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; + + QString val; + int valRow = 0; + ShiftItem *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(); + } + } + + if(item) + val = item->shiftName; + + //TODO: use a custom QRunnable + /* + QMetaObject::invokeMethod(this, "internalFetch", Qt::QueuedConnection, + Q_ARG(int, firstPendingRow), + Q_ARG(int, valRow), Q_ARG(QString, val)); + */ + internalFetch(firstPendingRow, valRow, val); +} + +void ShiftSQLModel::internalFetch(int first, int valRow, const QString& val) +{ + query q(mDb); + + int offset = first - valRow; + bool reverse = false; + + if(valRow > first) + { + offset = 0; + reverse = true; + } + + qDebug() << "Fetching:" << first << "ValRow:" << valRow << val << "Offset:" << offset << "Reverse:" << reverse; + + QByteArray sql = "SELECT id,name FROM jobshifts"; + if(!val.isEmpty()) + { + sql += " WHERE name"; + if(reverse) + sql += "?3"; + } + + if(!mQuery.isEmpty()) + { + if(val.isEmpty()) + sql += " WHERE name LIKE ?4"; + else + sql += " AND name LIKE ?4"; + } + + sql += " ORDER BY name"; + + 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); + + if(!val.isEmpty()) + q.bind(3, val); + + if(!mQuery.isEmpty()) + q.bind(4, mQuery); + + 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; + ShiftItem &item = vec[i]; + item.shiftId = r.get(0); + item.shiftName = r.get(1); + i--; + } + if(i > -1) + vec.remove(0, i + 1); + } + else + { + int i = 0; + + for(; it != end; ++it) + { + auto r = *it; + ShiftItem &item = vec[i]; + item.shiftId = r.get(0); + item.shiftName = r.get(1); + i++; + } + if(i < BatchSize) + vec.remove(i, BatchSize - i); + } + + + ShiftResultEvent *ev = new ShiftResultEvent; + ev->items = vec; + ev->firstRow = first; + + qApp->postEvent(this, ev); +} + +void ShiftSQLModel::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; + + QModelIndex firstIdx = index(firstRow, 0); + QModelIndex lastIdx = index(firstRow + items.count() - 1, NCols - 1); + emit dataChanged(firstIdx, lastIdx); + + qDebug() << "TOTAL: From:" << cacheFirstRow << "To:" << cacheFirstRow + cache.size() - 1; +} + +db_id ShiftSQLModel::shiftAtRow(int row) const +{ + if(row >= curItemCount || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return 0; //Not fetched yet or invalid + + return cache.at(row - cacheFirstRow).shiftId; +} + +QString ShiftSQLModel::shiftNameAtRow(int row) const +{ + if(row >= curItemCount || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return QString(); //Not fetched yet or invalid + + return cache.at(row - cacheFirstRow).shiftName; +} + +void ShiftSQLModel::setQuery(const QString &text) +{ + QString tmp = '%' + text + '%'; + if(mQuery == tmp) + return; + mQuery = tmp; + + refreshData(); +} + +bool ShiftSQLModel::removeShift(db_id shiftId) +{ + if(!shiftId) + return false; + + command cmd(mDb, "DELETE FROM jobshifts WHERE id=?"); + cmd.bind(1, shiftId); + int ret = cmd.execute(); + if(ret != SQLITE_OK) + { + qWarning() << "ShiftModel: error removing shift" << ret << mDb.error_msg(); + return false; + } + + emit Session->shiftRemoved(shiftId); + + refreshData(); + return true; +} + +bool ShiftSQLModel::removeShiftAt(int row) +{ + if(row >= curItemCount || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + return removeShift(cache.at(row - cacheFirstRow).shiftId); +} + +db_id ShiftSQLModel::addShift(int *outRow) +{ + db_id shiftId = 0; + + command cmd(mDb, "INSERT INTO jobshifts(id, name) VALUES(NULL, '')"); + sqlite3_mutex *mutex = sqlite3_db_mutex(mDb.db()); + sqlite3_mutex_enter(mutex); + int ret = cmd.execute(); + if(ret == SQLITE_OK) + { + shiftId = mDb.last_insert_rowid(); + } + sqlite3_mutex_leave(mutex); + + if(ret == SQLITE_CONSTRAINT_UNIQUE) + { + //There is already an empty shift, use that instead + query findEmpty(mDb, "SELECT id FROM jobshifts WHERE name = '' OR name IS NULL LIMIT 1"); + if(findEmpty.step() == SQLITE_ROW) + { + shiftId = findEmpty.getRows().get(0); + } + } + else if(ret != SQLITE_OK) + { + qDebug() << "Shift Error adding:" << ret; + } + + //Reset filter + mQuery.clear(); + mQuery.squeeze(); + + refreshData(); //Recalc row count + + if(outRow) + *outRow = shiftId ? 0 : -1; //Empty name is always the first + + if(shiftId) + emit Session->shiftAdded(shiftId); + + return shiftId; +} diff --git a/src/shifts/shiftsqlmodel.h b/src/shifts/shiftsqlmodel.h new file mode 100644 index 0000000..665580d --- /dev/null +++ b/src/shifts/shiftsqlmodel.h @@ -0,0 +1,81 @@ +#ifndef SHIFTSQLMODEL_H +#define SHIFTSQLMODEL_H + +#include "utils/sqldelegate/pageditemmodel.h" + +#include "utils/types.h" + +#include + +#include "shiftitem.h" + +//FIXME: remove old shift model +class ShiftSQLModel : public IPagedItemModel +{ + Q_OBJECT + +public: + enum Columns + { + ShiftName = 0, + NCols + }; + + enum { BatchSize = 100 }; + + ShiftSQLModel(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; + + // Editable: + bool setData(const QModelIndex &idx, const QVariant &value, + int role = Qt::EditRole) override; + + Qt::ItemFlags flags(const QModelIndex& idx) 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; + + // ShiftSQLModel + db_id shiftAtRow(int row) const; + QString shiftNameAtRow(int row) const; + + bool removeShift(db_id shiftId); + bool removeShiftAt(int row); + + db_id addShift(int *outRow); + +public slots: + void setQuery(const QString& text); + +private: + void fetchRow(int row); + Q_INVOKABLE void internalFetch(int first, int valRow, const QString &val); + void handleResult(const QVector items, int firstRow); + +private: + QVector cache; + + QString mQuery; + + int cacheFirstRow; + int firstPendingRow; +}; + +#endif // SHIFTSQLMODEL_H diff --git a/src/shifts/shiftviewer.cpp b/src/shifts/shiftviewer.cpp new file mode 100644 index 0000000..9af148f --- /dev/null +++ b/src/shifts/shiftviewer.cpp @@ -0,0 +1,81 @@ +#include "shiftviewer.h" + +#include "app/session.h" + +#include "shiftjobsmodel.h" + +#include +#include + +#include + +#include "viewmanager/viewmanager.h" + +ShiftViewer::ShiftViewer(QWidget *parent) : + QWidget(parent), + shiftId(0), + q_shiftName(Session->m_Db, "SELECT name FROM jobshifts WHERE id=?") +{ + QVBoxLayout *l = new QVBoxLayout(this); + + view = new QTableView(this); + l->addWidget(view); + + setMinimumSize(600, 200); + + model = new ShiftJobsModel(Session->m_Db, this); + view->setModel(model); + view->setContextMenuPolicy(Qt::CustomContextMenu); + connect(view, &QTableView::customContextMenuRequested, this, &ShiftViewer::showContextMenu); +} + +ShiftViewer::~ShiftViewer() +{ + +} + +void ShiftViewer::updateName() +{ + q_shiftName.bind(1, shiftId); + if(q_shiftName.step() != SQLITE_ROW) + { + //Error + } + else + { + setWindowTitle(q_shiftName.getRows().get(0)); + } + q_shiftName.reset(); +} + +void ShiftViewer::setShift(db_id id) +{ + shiftId = id; + updateName(); +} + +void ShiftViewer::updateJobsModel() +{ + model->loadShiftJobs(shiftId); +} + +void ShiftViewer::showContextMenu(const QPoint& pos) +{ + QModelIndex idx = view->indexAt(pos); + if(!idx.isValid()) + return; + + db_id jobId = model->getJobAt(idx.row()); + + QMenu menu(this); + + QAction *showInJobEditor = new QAction(tr("Show in Job Editor"), &menu); + menu.addAction(showInJobEditor); + + QAction *act = menu.exec(view->viewport()->mapToGlobal(pos)); + if(act == showInJobEditor) + { + //TODO: requestJobEditor() doesn't select item in graph + Session->getViewManager()->requestJobSelection(jobId, true, true); + } +} diff --git a/src/shifts/shiftviewer.h b/src/shifts/shiftviewer.h new file mode 100644 index 0000000..b09680f --- /dev/null +++ b/src/shifts/shiftviewer.h @@ -0,0 +1,38 @@ +#ifndef SHIFTVIEWER_H +#define SHIFTVIEWER_H + +#include + +#include "utils/types.h" + +#include + +using namespace sqlite3pp; + +class QTableView; +class ShiftJobsModel; + +class ShiftViewer : public QWidget +{ + Q_OBJECT +public: + ShiftViewer(QWidget *parent = nullptr); + virtual ~ShiftViewer(); + + void updateName(); + void updateJobsModel(); + void setShift(db_id id); + +private slots: + void showContextMenu(const QPoint &pos); + +private: + db_id shiftId; + + query q_shiftName; + + ShiftJobsModel *model; + QTableView *view; +}; + +#endif // SHIFTVIEWER_H diff --git a/src/sqlconsole/CMakeLists.txt b/src/sqlconsole/CMakeLists.txt new file mode 100644 index 0000000..cc6a0dc --- /dev/null +++ b/src/sqlconsole/CMakeLists.txt @@ -0,0 +1,11 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + sqlconsole/sqlconsole.h + sqlconsole/sqlresultmodel.h + sqlconsole/sqlviewer.h + + sqlconsole/sqlconsole.cpp + sqlconsole/sqlresultmodel.cpp + sqlconsole/sqlviewer.cpp + PARENT_SCOPE +) diff --git a/src/sqlconsole/sqlconsole.cpp b/src/sqlconsole/sqlconsole.cpp new file mode 100644 index 0000000..8fd9a4f --- /dev/null +++ b/src/sqlconsole/sqlconsole.cpp @@ -0,0 +1,99 @@ +#ifdef ENABLE_USER_QUERY + +#include "sqlconsole.h" + +#include +#include +#include +#include + +#include + +#include "sqlviewer.h" + +SQLConsole::SQLConsole(QWidget *parent) : + QDialog(parent), + timer(nullptr) +{ + setWindowTitle(QStringLiteral("SQL Console")); + + edit = new QPlainTextEdit; + viewer = new SQLViewer(nullptr); + QPushButton *clearBut = new QPushButton("Clear"); + QPushButton *runBut = new QPushButton("Run"); + + intervalSpin = new QSpinBox; + intervalSpin->setMinimum(0); + intervalSpin->setMaximum(10); + intervalSpin->setSuffix(" seconds"); + + QGridLayout *lay = new QGridLayout(this); + lay->addWidget(edit, 0, 0, 1, 3); + lay->addWidget(runBut, 1, 0, 1, 1); + lay->addWidget(clearBut, 1, 1, 1, 1); + lay->addWidget(intervalSpin, 1, 2, 1, 1); + lay->addWidget(viewer, 2, 0, 1, 3); + + connect(clearBut, &QPushButton::clicked, edit, &QPlainTextEdit::clear); + connect(runBut, &QPushButton::clicked, this, &SQLConsole::executeQuery); + + connect(intervalSpin, &QSpinBox::editingFinished, this, &SQLConsole::onIntervalChangedUser); + + setMinimumSize(400, 300); + setModal(false); +} + +SQLConsole::~SQLConsole() +{ + +} + +void SQLConsole::setInterval(int secs) +{ + if(secs == 0 && timer) + { + timer->stop(); + timer->deleteLater(); + timer = nullptr; + } + else if(secs != 0) + { + if(!timer) + { + timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, &SQLConsole::timedExec); + } + timer->start(secs * 1000); + intervalSpin->setValue(secs); + } +} + +void SQLConsole::onIntervalChangedUser() +{ + int secs = intervalSpin->value(); + if(timer && timer->interval() == secs) + return; + setInterval(secs); +} + +bool SQLConsole::executeQuery() +{ + QString sql = edit->toPlainText(); + if(!viewer->prepare(sql.toUtf8())) + return false; + + viewer->execQuery(); + return true; +} + +void SQLConsole::timedExec() +{ + if(!viewer->execQuery()) + { + setInterval(0); //Abort timer + return; + } + viewer->timedExec(); +} + +#endif // ENABLE_USER_QUERY diff --git a/src/sqlconsole/sqlconsole.h b/src/sqlconsole/sqlconsole.h new file mode 100644 index 0000000..133cc39 --- /dev/null +++ b/src/sqlconsole/sqlconsole.h @@ -0,0 +1,38 @@ +#ifndef SQLCONSOLE_H +#define SQLCONSOLE_H + +#ifdef ENABLE_USER_QUERY + +#include + +class SQLViewer; +class QPlainTextEdit; +class QSpinBox; +class QTimer; + +class SQLConsole : public QDialog +{ + Q_OBJECT +public: + explicit SQLConsole(QWidget *parent = nullptr); + ~SQLConsole(); + + void setInterval(int secs); + +public slots: + bool executeQuery(); + +private slots: + void onIntervalChangedUser(); + + void timedExec(); +private: + QPlainTextEdit *edit; + SQLViewer *viewer; + QSpinBox *intervalSpin; + QTimer *timer; +}; + +#endif // ENABLE_USER_QUERY + +#endif // SQLCONSOLE_H diff --git a/src/sqlconsole/sqlresultmodel.cpp b/src/sqlconsole/sqlresultmodel.cpp new file mode 100644 index 0000000..2e2012f --- /dev/null +++ b/src/sqlconsole/sqlresultmodel.cpp @@ -0,0 +1,140 @@ +#ifdef ENABLE_USER_QUERY + +#include "sqlresultmodel.h" + +#include + +#include "app/session.h" + +SQLResultModel::SQLResultModel(QObject *parent) : + QAbstractTableModel(parent), + colCount(0) +{ + +} + +SQLResultModel::~SQLResultModel() +{ + +} + +int SQLResultModel::rowCount(const QModelIndex &parent) const +{ + return (parent.isValid() || colCount == 0) ? 0 : m_data.size() / colCount; +} + +int SQLResultModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : colCount; +} + +QVariant SQLResultModel::data(const QModelIndex &idx, int role) const +{ + if(!idx.isValid() || !colCount || idx.row() >= (m_data.size() / colCount)) + return QVariant(); + + if(role == Qt::DisplayRole) + { + int i = idx.row() * colCount + idx.column(); + return m_data.at(i); + } + + if(role == Qt::BackgroundRole) + { + return backGround; + } + + return QVariant(); +} + +QVariant SQLResultModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + return colNames.value(section, "ERR"); + } + + return QAbstractTableModel::headerData(section, orientation, role); +} + +void SQLResultModel::setBackground(const QColor &col) +{ + beginResetModel(); + backGround = col; + endResetModel(); +} + +bool SQLResultModel::initFromQuery(sqlite3pp::query *q) +{ + beginResetModel(); + + colCount = q->column_count(); + + colNames.clear(); + colNames.reserve(colCount); + + m_data.clear(); + m_data.squeeze(); + m_data.reserve(colCount); + + for(int i = 0; i < colCount; i++) + { + colNames.append(q->column_name(i)); + } + + int ret = q->step(); + while (ret == SQLITE_ROW) + { + for(int c = 0; c < colCount; c++) + { + auto r = q->getRows(); + QVariant val; + int type = r.column_type(c); + + switch (type) + { + case SQLITE_INTEGER: + { + val = r.get(c); + break; + } + case SQLITE_TEXT: + { + val = r.get(c); + break; + } + case SQLITE_BLOB: + { + val = "BLOB " + r.get(c); + break; + } + case SQLITE_NULL: + { + val = QStringLiteral("NULL"); + break; + } + case SQLITE_FLOAT: + { + val = r.get(c); + break; + } + default: + break; + } + + m_data.append(val); + } + ret = q->step(); + } + + endResetModel(); + + if(ret != SQLITE_OK && ret != SQLITE_DONE) + { + emit error(ret, Session->m_Db.extended_error_code(), Session->m_Db.error_msg()); + return false; + } + return true; +} + +#endif // ENABLE_USER_QUERY diff --git a/src/sqlconsole/sqlresultmodel.h b/src/sqlconsole/sqlresultmodel.h new file mode 100644 index 0000000..3b01ee8 --- /dev/null +++ b/src/sqlconsole/sqlresultmodel.h @@ -0,0 +1,43 @@ +#ifndef SQLRESULTMODEL_H +#define SQLRESULTMODEL_H + +#ifdef ENABLE_USER_QUERY + +#include +#include + +namespace sqlite3pp { +class query; +} + +class SQLResultModel : public QAbstractTableModel +{ + Q_OBJECT +public: + SQLResultModel(QObject *parent = nullptr); + virtual ~SQLResultModel() 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; + + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + + void setBackground(const QColor& col); + bool initFromQuery(sqlite3pp::query *q); + +signals: + void error(int err, int extendedErr, const QString& msg); + +private: + QStringList colNames; + QVector m_data; + int colCount; + QColor backGround; +}; + +#endif // ENABLE_USER_QUERY + +#endif // SQLRESULTMODEL_H diff --git a/src/sqlconsole/sqlviewer.cpp b/src/sqlconsole/sqlviewer.cpp new file mode 100644 index 0000000..a4a743a --- /dev/null +++ b/src/sqlconsole/sqlviewer.cpp @@ -0,0 +1,106 @@ +#ifdef ENABLE_USER_QUERY + +#include "sqlviewer.h" + +#include "app/session.h" + +#include "sqlresultmodel.h" +#include + +#include +#include +#include + +#include + +#include + +SQLViewer::SQLViewer(sqlite3pp::query *q, QWidget *parent) : + QWidget (parent), + model(nullptr), + mQuery(nullptr), + view(nullptr), + deleteQuery(false) +{ + setQuery(q); + setWindowTitle(QStringLiteral("SQL Viewer")); + view = new QTableView(this); + QVBoxLayout *l = new QVBoxLayout(this); + l->addWidget(view); + + model = new SQLResultModel(this); + connect(model, &SQLResultModel::error, this, &SQLViewer::onError); + resetColor(); + view->setModel(model); + view->setEditTriggers(QTableView::NoEditTriggers); + + setMinimumSize(300, 200); +} + +SQLViewer::~SQLViewer() +{ + if(deleteQuery) + delete mQuery; +} + +bool SQLViewer::execQuery() +{ + bool ret = false; + { + mQuery->reset(); + ret = model->initFromQuery(mQuery); + mQuery->reset(); + } + return ret; +} + +void SQLViewer::timedExec() +{ + QColor c(255, 0, 0, 50); + model->setBackground(c); + QTimer::singleShot(300, this, &SQLViewer::resetColor); +} + +void SQLViewer::resetColor() +{ + model->setBackground(Qt::white); +} + +void SQLViewer::onError(int err, int extended, const QString &msg) +{ + showErrorMsg(tr("Query Error"), msg, err, extended); +} + +void SQLViewer::showErrorMsg(const QString& title, const QString& msg, int err, int extendedErr) +{ + QMessageBox::warning(this, title, + tr("SQLite Error: %1\n" + "Extended: %2\n" + "Message: %3") + .arg(err) + .arg(extendedErr) + .arg(msg)); +} + +void SQLViewer::setQuery(sqlite3pp::query *query) +{ + if(deleteQuery) + delete mQuery; + mQuery = query; + deleteQuery = !mQuery; + if(!mQuery) + mQuery = new sqlite3pp::query(Session->m_Db); +} + +bool SQLViewer::prepare(const QByteArray& sql) +{ + int ret = mQuery->prepare(sql); + if(ret != SQLITE_OK) + { + showErrorMsg(tr("Preparation Failed"), Session->m_Db.error_msg(), ret, Session->m_Db.extended_error_code()); + return false; + } + return true; +} + +#endif // ENABLE_USER_QUERY diff --git a/src/sqlconsole/sqlviewer.h b/src/sqlconsole/sqlviewer.h new file mode 100644 index 0000000..8c88c7d --- /dev/null +++ b/src/sqlconsole/sqlviewer.h @@ -0,0 +1,42 @@ +#ifndef SQLVIEWER_H +#define SQLVIEWER_H + +#ifdef ENABLE_USER_QUERY + +#include + +class QTableView; +class SQLResultModel; + +namespace sqlite3pp { +class query; +} + +class SQLViewer : public QWidget +{ + Q_OBJECT +public: + SQLViewer(sqlite3pp::query *q, QWidget *parent = nullptr); + virtual ~SQLViewer(); + + void showErrorMsg(const QString &title, const QString &msg, int err, int extendedErr); + + void setQuery(sqlite3pp::query *query); + bool prepare(const QByteArray &sql); +public slots: + bool execQuery(); + void timedExec(); + void resetColor(); + + void onError(int err, int extended, const QString& msg); + +private: + SQLResultModel *model; + sqlite3pp::query *mQuery; + QTableView *view; + bool deleteQuery; +}; + +#endif // ENABLE_USER_QUERY + +#endif // SQLVIEWER_H diff --git a/src/sqlite3pp/CMakeLists.txt b/src/sqlite3pp/CMakeLists.txt new file mode 100644 index 0000000..2b5d29f --- /dev/null +++ b/src/sqlite3pp/CMakeLists.txt @@ -0,0 +1,8 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + sqlite3pp/sqlite3pp.h + sqlite3pp/sqlite3pp.ipp + sqlite3pp/sqlite3ppext.h + sqlite3pp/sqlite3ppext.ipp + PARENT_SCOPE +) diff --git a/src/sqlite3pp/sqlite3pp.h b/src/sqlite3pp/sqlite3pp.h new file mode 100644 index 0000000..e893559 --- /dev/null +++ b/src/sqlite3pp/sqlite3pp.h @@ -0,0 +1,389 @@ +// sqlite3pp.h +// +// The MIT License +// +// Copyright (c) 2015 Wongoo Lee (iwongu at gmail dot com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#ifndef SQLITE3PP_H +#define SQLITE3PP_H + +#define SQLITE3PP_VERSION "1.0.6" +#define SQLITE3PP_VERSION_MAJOR 1 +#define SQLITE3PP_VERSION_MINOR 0 +#define SQLITE3PP_VERSION_PATCH 6 + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace sqlite3pp +{ + namespace ext + { + class function; + class aggregate; + } + + template + struct convert { + using to_int = int; + }; + + class null_type {}; + + class noncopyable + { + protected: + noncopyable() = default; + ~noncopyable() = default; + + noncopyable(noncopyable&&) = default; + noncopyable& operator=(noncopyable&&) = default; + + noncopyable(noncopyable const&) = delete; + noncopyable& operator=(noncopyable const&) = delete; + }; + + class database : noncopyable + { + friend class statement; + friend class database_error; + friend class ext::function; + friend class ext::aggregate; + + public: + using busy_handler = std::function; + using commit_handler = std::function; + using rollback_handler = std::function; + using update_handler = std::function; + using authorize_handler = std::function; + using backup_handler = std::function; + + explicit database(char const* dbname = nullptr, int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, const char* vfs = nullptr); + + database(database&& db); + database& operator=(database&& db); + + ~database(); + + int connect(char const* dbname, int flags, const char* vfs = nullptr); + int disconnect(); + + int attach(char const* dbname, char const* name); + int detach(char const* name); + + int backup(database& destdb, backup_handler h = {}); + int backup(char const* dbname, database& destdb, char const* destdbname, backup_handler h, int step_page = 5); + + //NOTE: use inside sqlite3_db_mutex + long long int last_insert_rowid() const; + + int enable_foreign_keys(bool enable = true); + int enable_triggers(bool enable = true); + int enable_extended_result_codes(bool enable = true); + + int changes() const; + + int error_code() const; + int extended_error_code() const; + char const* error_msg() const; + + int execute(char const* sql); + int executef(char const* sql, ...); + + int set_busy_timeout(int ms); + + void set_busy_handler(busy_handler h); + void set_commit_handler(commit_handler h); + void set_rollback_handler(rollback_handler h); + void set_update_handler(update_handler h); + void set_authorize_handler(authorize_handler h); + + inline sqlite3 *db() const + { + return db_; + } + + private: + sqlite3* db_; + + busy_handler bh_; + commit_handler ch_; + rollback_handler rh_; + update_handler uh_; + authorize_handler ah_; + }; + + class database_error : public std::runtime_error + { + public: + explicit database_error(char const* msg); + explicit database_error(database& db); + }; + + enum copy_semantic { copy, nocopy }; + + class statement : noncopyable + { + public: + int prepare(char const* stmt); + int finish(); + + int bind(int idx, int value); + int bind(int idx, double value); + int bind(int idx, long long int value); + int bind(int idx, char const* value, copy_semantic fcopy); + int bind(int idx, void const* value, int n, copy_semantic fcopy); + int bind(int idx, std::string const& value, copy_semantic fcopy); + int bind(int idx); + int bind(int idx, null_type); + + int bind(char const* name, int value); + int bind(char const* name, double value); + int bind(char const* name, long long int value); + int bind(char const* name, char const* value, copy_semantic fcopy); + int bind(char const* name, void const* value, int n, copy_semantic fcopy); + int bind(char const* name, std::string const& value, copy_semantic fcopy); + int bind(char const* name); + int bind(char const* name, null_type); + + inline int bind(int idx, const QTime& t) + { + int minutes = t.msecsSinceStartOfDay() / 60000; //msecs to minutes + return bind(idx, minutes); + } + + //BIG TODO: is this function better? + //Binding 0 or NULL is the same? + inline int bindOrNull(int idx, long long int value) + { + if(value) + return bind(idx, value); + return bind(idx); //Bind null + } + + int bind(int idx, const QString& str) + { + QByteArray arr = str.toUtf8(); + return sqlite3_bind_text(stmt_, idx, arr.data(), arr.size(), SQLITE_TRANSIENT); + } + + int step(); + int reset(); + + sqlite3_stmt *stmt() const + { + return stmt_; + } + + explicit statement(database& db, char const* stmt = nullptr); + ~statement(); + + protected: + + + int prepare_impl(char const* stmt); + int finish_impl(sqlite3_stmt* stmt); + + protected: + database& db_; + sqlite3_stmt* stmt_; + char const* tail_; + }; + + class command : public statement + { + public: + class bindstream + { + public: + bindstream(command& cmd, int idx); + + template + bindstream& operator << (T value) { + auto rc = cmd_.bind(idx_, value); + if (rc != SQLITE_OK) { + throw database_error(cmd_.db_); + } + ++idx_; + return *this; + } + bindstream& operator << (char const* value) { + auto rc = cmd_.bind(idx_, value, copy); + if (rc != SQLITE_OK) { + throw database_error(cmd_.db_); + } + ++idx_; + return *this; + } + bindstream& operator << (std::string const& value) { + auto rc = cmd_.bind(idx_, value, copy); + if (rc != SQLITE_OK) { + throw database_error(cmd_.db_); + } + ++idx_; + return *this; + } + + private: + command& cmd_; + int idx_; + }; + + explicit command(database& db, char const* stmt = nullptr); + + bindstream binder(int idx = 1); + + int execute(); + int execute_all(); + }; + + class query : public statement + { + public: + class rows + { + public: + class getstream + { + public: + getstream(rows* rws, int idx); + + template + getstream& operator >> (T& value) { + value = rws_->get(idx_, T()); + ++idx_; + return *this; + } + + private: + rows* rws_; + int idx_; + }; + + explicit rows(sqlite3_stmt* stmt); + + int data_count() const; + int column_type(int idx) const; + + int column_bytes(int idx) const; + + //FIXME: use directly templates + template T get(int idx) const { + return get(idx, T()); + } + + //FIXME: remove + template + std::tuple get_columns(typename convert::to_int... idxs) const { + return std::make_tuple(get(idxs, Ts())...); + } + + getstream getter(int idx = 0); + + private: + int get(int idx, int) const; + double get(int idx, double) const; + long long int get(int idx, long long int) const; + char const* get(int idx, char const*) const; + std::string get(int idx, std::string) const; + void const* get(int idx, void const*) const; + null_type get(int idx, null_type) const; + + inline QString get(int idx, QString) const + { + const int len = sqlite3_column_bytes(stmt_, idx); + const char *text = reinterpret_cast(sqlite3_column_text(stmt_, idx)); + return QString::fromUtf8(text, len); + } + + inline QTime get(int idx, QTime) const + { + const int msecs = get(idx) * 60000; //Minutes to msecs + return QTime::fromMSecsSinceStartOfDay(msecs); + } + + private: + sqlite3_stmt* stmt_; + }; + + class query_iterator + : public std::iterator + { + public: + query_iterator(); + explicit query_iterator(query* cmd); + + bool operator==(query_iterator const&) const; + bool operator!=(query_iterator const&) const; + + query_iterator& operator++(); + + value_type operator*() const; + + private: + query* cmd_; + int rc_; + }; + + explicit query(database& db, char const* stmt = nullptr); + + int column_count() const; + + char const* column_name(int idx) const; + char const* column_decltype(int idx) const; + + using iterator = query_iterator; + + iterator begin(); + iterator end(); + + rows getRows() + { + return rows(stmt_); + } + }; + + class transaction : noncopyable + { + public: + explicit transaction(database& db, bool fcommit = false, bool freserve = false); + ~transaction(); + + int commit(); + int rollback(); + + private: + database* db_; + bool fcommit_; + }; + +} // namespace sqlite3pp + +#include "sqlite3pp.ipp" + +#endif diff --git a/src/sqlite3pp/sqlite3pp.ipp b/src/sqlite3pp/sqlite3pp.ipp new file mode 100644 index 0000000..cd5f29d --- /dev/null +++ b/src/sqlite3pp/sqlite3pp.ipp @@ -0,0 +1,610 @@ +// sqlite3pp.cpp +// +// The MIT License +// +// Copyright (c) 2015 Wongoo Lee (iwongu at gmail dot com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include +#include + +namespace sqlite3pp +{ + + namespace + { + null_type ignore; + + int busy_handler_impl(void* p, int cnt) + { + auto h = static_cast(p); + return (*h)(cnt); + } + + int commit_hook_impl(void* p) + { + auto h = static_cast(p); + return (*h)(); + } + + void rollback_hook_impl(void* p) + { + auto h = static_cast(p); + (*h)(); + } + + void update_hook_impl(void* p, int opcode, char const* dbname, char const* tablename, long long int rowid) + { + auto h = static_cast(p); + (*h)(opcode, dbname, tablename, rowid); + } + + int authorizer_impl(void* p, int evcode, char const* p1, char const* p2, char const* dbname, char const* tvname) + { + auto h = static_cast(p); + return (*h)(evcode, p1, p2, dbname, tvname); + } + + } // namespace + + inline database::database(char const* dbname, int flags, char const* vfs) : db_(nullptr) + { + if (dbname) { + auto rc = connect(dbname, flags, vfs); + if (rc != SQLITE_OK) + throw database_error("can't connect database"); + } + } + + inline database::database(database&& db) : db_(std::move(db.db_)), + bh_(std::move(db.bh_)), + ch_(std::move(db.ch_)), + rh_(std::move(db.rh_)), + uh_(std::move(db.uh_)), + ah_(std::move(db.ah_)) + { + db.db_ = nullptr; + } + + inline database& database::operator=(database&& db) + { + db_ = std::move(db.db_); + db.db_ = nullptr; + + bh_ = std::move(db.bh_); + ch_ = std::move(db.ch_); + rh_ = std::move(db.rh_); + uh_ = std::move(db.uh_); + ah_ = std::move(db.ah_); + + return *this; + } + + inline database::~database() + { + disconnect(); + } + + inline int database::connect(char const* dbname, int flags, char const* vfs) + { + disconnect(); + + return sqlite3_open_v2(dbname, &db_, flags, vfs); + } + + inline int database::disconnect() + { + auto rc = SQLITE_OK; + if (db_) { + rc = sqlite3_close(db_); + if (rc == SQLITE_OK) { + db_ = nullptr; + } + } + + return rc; + } + + inline int database::attach(char const* dbname, char const* name) + { + return executef("ATTACH '%q' AS '%q'", dbname, name); + } + + inline int database::detach(char const* name) + { + return executef("DETACH '%q'", name); + } + + inline int database::backup(database& destdb, backup_handler h) + { + return backup("main", destdb, "main", h); + } + + inline int database::backup(char const* dbname, database& destdb, char const* destdbname, backup_handler h, int step_page) + { + sqlite3_backup* bkup = sqlite3_backup_init(destdb.db_, destdbname, db_, dbname); + if (!bkup) { + return error_code(); + } + auto rc = SQLITE_OK; + do { + rc = sqlite3_backup_step(bkup, step_page); + if (h) { + h(sqlite3_backup_remaining(bkup), sqlite3_backup_pagecount(bkup), rc); + } + } while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED); + sqlite3_backup_finish(bkup); + return rc; + } + + inline void database::set_busy_handler(busy_handler h) + { + bh_ = h; + sqlite3_busy_handler(db_, bh_ ? busy_handler_impl : 0, &bh_); + } + + inline void database::set_commit_handler(commit_handler h) + { + ch_ = h; + sqlite3_commit_hook(db_, ch_ ? commit_hook_impl : 0, &ch_); + } + + inline void database::set_rollback_handler(rollback_handler h) + { + rh_ = h; + sqlite3_rollback_hook(db_, rh_ ? rollback_hook_impl : 0, &rh_); + } + + inline void database::set_update_handler(update_handler h) + { + uh_ = h; + sqlite3_update_hook(db_, uh_ ? update_hook_impl : 0, &uh_); + } + + inline void database::set_authorize_handler(authorize_handler h) + { + ah_ = h; + sqlite3_set_authorizer(db_, ah_ ? authorizer_impl : 0, &ah_); + } + + inline long long int database::last_insert_rowid() const + { + return sqlite3_last_insert_rowid(db_); + } + + inline int database::enable_foreign_keys(bool enable) + { + return sqlite3_db_config(db_, SQLITE_DBCONFIG_ENABLE_FKEY, enable ? 1 : 0, nullptr); + } + + inline int database::enable_triggers(bool enable) + { + return sqlite3_db_config(db_, SQLITE_DBCONFIG_ENABLE_TRIGGER, enable ? 1 : 0, nullptr); + } + + inline int database::enable_extended_result_codes(bool enable) + { + return sqlite3_extended_result_codes(db_, enable ? 1 : 0); + } + + inline int database::changes() const + { + return sqlite3_changes(db_); + } + + inline int database::error_code() const + { + return sqlite3_errcode(db_); + } + + inline int database::extended_error_code() const + { + return sqlite3_extended_errcode(db_); + } + + inline char const* database::error_msg() const + { + return sqlite3_errmsg(db_); + } + + inline int database::execute(char const* sql) + { + return sqlite3_exec(db_, sql, 0, 0, 0); + } + + inline int database::executef(char const* sql, ...) + { + va_list ap; + va_start(ap, sql); + std::shared_ptr msql(sqlite3_vmprintf(sql, ap), sqlite3_free); + va_end(ap); + + return execute(msql.get()); + } + + inline int database::set_busy_timeout(int ms) + { + return sqlite3_busy_timeout(db_, ms); + } + + + inline statement::statement(database& db, char const* stmt) : db_(db), stmt_(0), tail_(0) + { + if (stmt) { + auto rc = prepare(stmt); + if (rc != SQLITE_OK) + throw database_error(db_); + } + } + + inline statement::~statement() + { + // finish() can return error. If you want to check the error, call + // finish() explicitly before this object is destructed. + finish(); + } + + inline int statement::prepare(char const* stmt) + { + auto rc = finish(); + if (rc != SQLITE_OK) + return rc; + + return prepare_impl(stmt); + } + + inline int statement::prepare_impl(char const* stmt) + { + return sqlite3_prepare(db_.db_, stmt, std::strlen(stmt), &stmt_, &tail_); + } + + inline int statement::finish() + { + auto rc = SQLITE_OK; + if (stmt_) { + rc = finish_impl(stmt_); + stmt_ = nullptr; + } + tail_ = nullptr; + + return rc; + } + + inline int statement::finish_impl(sqlite3_stmt* stmt) + { + return sqlite3_finalize(stmt); + } + + inline int statement::step() + { + return sqlite3_step(stmt_); + } + + inline int statement::reset() + { + return sqlite3_reset(stmt_); + } + + inline int statement::bind(int idx, int value) + { + return sqlite3_bind_int(stmt_, idx, value); + } + + inline int statement::bind(int idx, double value) + { + return sqlite3_bind_double(stmt_, idx, value); + } + + inline int statement::bind(int idx, long long int value) + { + return sqlite3_bind_int64(stmt_, idx, value); + } + + inline int statement::bind(int idx, char const* value, copy_semantic fcopy) + { + return sqlite3_bind_text(stmt_, idx, value, std::strlen(value), fcopy == copy ? SQLITE_TRANSIENT : SQLITE_STATIC ); + } + + inline int statement::bind(int idx, void const* value, int n, copy_semantic fcopy) + { + return sqlite3_bind_blob(stmt_, idx, value, n, fcopy == copy ? SQLITE_TRANSIENT : SQLITE_STATIC ); + } + + inline int statement::bind(int idx, std::string const& value, copy_semantic fcopy) + { + return sqlite3_bind_text(stmt_, idx, value.c_str(), value.size(), fcopy == copy ? SQLITE_TRANSIENT : SQLITE_STATIC ); + } + + inline int statement::bind(int idx) + { + return sqlite3_bind_null(stmt_, idx); + } + + inline int statement::bind(int idx, null_type) + { + return bind(idx); + } + + inline int statement::bind(char const* name, int value) + { + auto idx = sqlite3_bind_parameter_index(stmt_, name); + return bind(idx, value); + } + + inline int statement::bind(char const* name, double value) + { + auto idx = sqlite3_bind_parameter_index(stmt_, name); + return bind(idx, value); + } + + inline int statement::bind(char const* name, long long int value) + { + auto idx = sqlite3_bind_parameter_index(stmt_, name); + return bind(idx, value); + } + + inline int statement::bind(char const* name, char const* value, copy_semantic fcopy) + { + auto idx = sqlite3_bind_parameter_index(stmt_, name); + return bind(idx, value, fcopy); + } + + inline int statement::bind(char const* name, void const* value, int n, copy_semantic fcopy) + { + auto idx = sqlite3_bind_parameter_index(stmt_, name); + return bind(idx, value, n, fcopy); + } + + inline int statement::bind(char const* name, std::string const& value, copy_semantic fcopy) + { + auto idx = sqlite3_bind_parameter_index(stmt_, name); + return bind(idx, value, fcopy); + } + + inline int statement::bind(char const* name) + { + auto idx = sqlite3_bind_parameter_index(stmt_, name); + return bind(idx); + } + + inline int statement::bind(char const* name, null_type) + { + return bind(name); + } + + + inline command::bindstream::bindstream(command& cmd, int idx) : cmd_(cmd), idx_(idx) + { + } + + inline command::command(database& db, char const* stmt) : statement(db, stmt) + { + } + + inline command::bindstream command::binder(int idx) + { + return bindstream(*this, idx); + } + + inline int command::execute() + { + auto rc = step(); + if (rc == SQLITE_DONE) rc = SQLITE_OK; + + return rc; + } + + inline int command::execute_all() + { + auto rc = execute(); + if (rc != SQLITE_OK) return rc; + + char const* sql = tail_; + + while (std::strlen(sql) > 0) { // sqlite3_complete() is broken. + sqlite3_stmt* old_stmt = stmt_; + + if ((rc = prepare_impl(sql)) != SQLITE_OK) return rc; + + if ((rc = sqlite3_transfer_bindings(old_stmt, stmt_)) != SQLITE_OK) return rc; + + finish_impl(old_stmt); + + if ((rc = execute()) != SQLITE_OK) return rc; + + sql = tail_; + } + + return rc; + } + + + inline query::rows::getstream::getstream(rows* rws, int idx) : rws_(rws), idx_(idx) + { + } + + inline query::rows::rows(sqlite3_stmt* stmt) : stmt_(stmt) + { + } + + inline int query::rows::data_count() const + { + return sqlite3_data_count(stmt_); + } + + inline int query::rows::column_type(int idx) const + { + return sqlite3_column_type(stmt_, idx); + } + + inline int query::rows::column_bytes(int idx) const + { + return sqlite3_column_bytes(stmt_, idx); + } + + inline int query::rows::get(int idx, int) const + { + return sqlite3_column_int(stmt_, idx); + } + + inline double query::rows::get(int idx, double) const + { + return sqlite3_column_double(stmt_, idx); + } + + inline long long int query::rows::get(int idx, long long int) const + { + return sqlite3_column_int64(stmt_, idx); + } + + inline char const* query::rows::get(int idx, char const*) const + { + return reinterpret_cast(sqlite3_column_text(stmt_, idx)); + } + + inline std::string query::rows::get(int idx, std::string) const + { + return get(idx, (char const*)0); + } + + inline void const* query::rows::get(int idx, void const*) const + { + return sqlite3_column_blob(stmt_, idx); + } + + inline null_type query::rows::get(int /*idx*/, null_type) const + { + return ignore; + } + + inline query::rows::getstream query::rows::getter(int idx) + { + return getstream(this, idx); + } + + inline query::query_iterator::query_iterator() : cmd_(0) + { + rc_ = SQLITE_DONE; + } + + inline query::query_iterator::query_iterator(query* cmd) : cmd_(cmd) + { + rc_ = cmd_->step(); + if (rc_ != SQLITE_ROW && rc_ != SQLITE_DONE) + throw database_error(cmd_->db_); + } + + inline bool query::query_iterator::operator==(query::query_iterator const& other) const + { + return rc_ == other.rc_; + } + + inline bool query::query_iterator::operator!=(query::query_iterator const& other) const + { + return rc_ != other.rc_; + } + + inline query::query_iterator& query::query_iterator::operator++() + { + rc_ = cmd_->step(); + if (rc_ != SQLITE_ROW && rc_ != SQLITE_DONE) + throw database_error(cmd_->db_); + return *this; + } + + inline query::query_iterator::value_type query::query_iterator::operator*() const + { + return rows(cmd_->stmt_); + } + + inline query::query(database& db, char const* stmt) : statement(db, stmt) + { + } + + inline int query::column_count() const + { + return sqlite3_column_count(stmt_); + } + + inline char const* query::column_name(int idx) const + { + return sqlite3_column_name(stmt_, idx); + } + + inline char const* query::column_decltype(int idx) const + { + return sqlite3_column_decltype(stmt_, idx); + } + + + inline query::iterator query::begin() + { + return query_iterator(this); + } + + inline query::iterator query::end() + { + return query_iterator(); + } + + + inline transaction::transaction(database& db, bool fcommit, bool freserve) : db_(&db), fcommit_(fcommit) + { + int rc = db_->execute(freserve ? "BEGIN IMMEDIATE" : "BEGIN"); + if (rc != SQLITE_OK) + throw database_error(*db_); + } + + inline transaction::~transaction() + { + if (db_) { + // execute() can return error. If you want to check the error, + // call commit() or rollback() explicitly before this object is + // destructed. + db_->execute(fcommit_ ? "COMMIT" : "ROLLBACK"); + } + } + + inline int transaction::commit() + { + auto db = db_; + db_ = nullptr; + int rc = db->execute("COMMIT"); + return rc; + } + + inline int transaction::rollback() + { + auto db = db_; + db_ = nullptr; + int rc = db->execute("ROLLBACK"); + return rc; + } + + + inline database_error::database_error(char const* msg) : std::runtime_error(msg) + { + } + + inline database_error::database_error(database& db) : std::runtime_error(sqlite3_errmsg(db.db_)) + { + } + +} // namespace sqlite3pp diff --git a/src/sqlite3pp/sqlite3ppext.h b/src/sqlite3pp/sqlite3ppext.h new file mode 100644 index 0000000..f0aefcf --- /dev/null +++ b/src/sqlite3pp/sqlite3ppext.h @@ -0,0 +1,233 @@ +// sqlite3ppext.h +// +// The MIT License +// +// Copyright (c) 2015 Wongoo Lee (iwongu at gmail dot com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#ifndef SQLITE3PPEXT_H +#define SQLITE3PPEXT_H + +#include +#include +#include +#include +#include +#include + +#include "sqlite3pp.h" + +namespace sqlite3pp +{ + namespace + { + template + struct Apply { + template + static inline auto apply(F&& f, T&& t, A&&... a) + -> decltype(Apply::apply(std::forward(f), + std::forward(t), + std::get(std::forward(t)), + std::forward(a)...)) + { + return Apply::apply(std::forward(f), + std::forward(t), + std::get(std::forward(t)), + std::forward(a)...); + } + }; + + template<> + struct Apply<0> { + template + static inline auto apply(F&& f, T&&, A&&... a) + -> decltype(std::forward(f)(std::forward(a)...)) + { + return std::forward(f)(std::forward(a)...); + } + }; + + template + inline auto apply(F&& f, T&& t) + -> decltype(Apply::type>::value>::apply(std::forward(f), std::forward(t))) + { + return Apply::type>::value>::apply( + std::forward(f), std::forward(t)); + } + } + + + namespace ext + { + + class context : noncopyable + { + public: + explicit context(sqlite3_context* ctx, int nargs = 0, sqlite3_value** values = nullptr); + + int args_count() const; + int args_bytes(int idx) const; + int args_type(int idx) const; + + template T get(int idx) const { + return get(idx, T()); + } + + void result(int value); + void result(double value); + void result(long long int value); + void result(std::string const& value); + void result(char const* value, bool fcopy); + void result(void const* value, int n, bool fcopy); + void result(); + void result(null_type); + void result_copy(int idx); + void result_error(char const* msg); + + void* aggregate_data(int size); + int aggregate_count(); + + template + std::tuple to_tuple() { + return to_tuple_impl(0, *this, std::tuple()); + } + + private: + int get(int idx, int) const; + double get(int idx, double) const; + long long int get(int idx, long long int) const; + char const* get(int idx, char const*) const; + std::string get(int idx, std::string) const; + void const* get(int idx, void const*) const; + + template + static inline std::tuple to_tuple_impl(int index, const context& c, std::tuple&&) + { + auto h = std::make_tuple(c.context::get(index)); + return std::tuple_cat(h, to_tuple_impl(++index, c, std::tuple())); + } + static inline std::tuple<> to_tuple_impl(int /*index*/, const context& /*c*/, std::tuple<>&&) + { + return std::tuple<>(); + } + + private: + sqlite3_context* ctx_; + int nargs_; + sqlite3_value** values_; + }; + + namespace + { + template + void functionx_impl(sqlite3_context* ctx, int nargs, sqlite3_value** values) + { + context c(ctx, nargs, values); + auto f = static_cast*>(sqlite3_user_data(ctx)); + c.result(apply(*f, c.to_tuple())); + } + } + + class function : noncopyable + { + public: + using function_handler = std::function; + using pfunction_base = std::shared_ptr; + + explicit function(database& db); + + int create(char const* name, function_handler h, int nargs = 0); + + template int create(char const* name, std::function h) { + fh_[name] = std::shared_ptr(new std::function(h)); + return create_function_impl()(db_, fh_[name].get(), name); + } + + private: + + template + struct create_function_impl; + + template + struct create_function_impl + { + int operator()(sqlite3* db, void* fh, char const* name) { + return sqlite3_create_function(db, name, sizeof...(Ps), SQLITE_UTF8, fh, + functionx_impl, + 0, 0); + } + }; + + private: + sqlite3* db_; + + std::map fh_; + }; + + namespace + { + template + void stepx_impl(sqlite3_context* ctx, int nargs, sqlite3_value** values) + { + context c(ctx, nargs, values); + T* t = static_cast(c.aggregate_data(sizeof(T))); + if (c.aggregate_count() == 1) new (t) T; + apply([](T* tt, Ps... ps){tt->step(ps...);}, + std::tuple_cat(std::make_tuple(t), c.to_tuple())); + } + + template + void finishN_impl(sqlite3_context* ctx) + { + context c(ctx); + T* t = static_cast(c.aggregate_data(sizeof(T))); + c.result(t->finish()); + t->~T(); + } + } + + class aggregate : noncopyable + { + public: + using function_handler = std::function; + using pfunction_base = std::shared_ptr; + + explicit aggregate(database& db); + + int create(char const* name, function_handler s, function_handler f, int nargs = 1); + + template + int create(char const* name) { + return sqlite3_create_function(db_, name, sizeof...(Ps), SQLITE_UTF8, 0, 0, stepx_impl, finishN_impl); + } + + private: + sqlite3* db_; + + std::map > ah_; + }; + + } // namespace ext + +} // namespace sqlite3pp + +#include "sqlite3ppext.ipp" + +#endif diff --git a/src/sqlite3pp/sqlite3ppext.ipp b/src/sqlite3pp/sqlite3ppext.ipp new file mode 100644 index 0000000..2b683bc --- /dev/null +++ b/src/sqlite3pp/sqlite3ppext.ipp @@ -0,0 +1,195 @@ +// sqlite3ppext.cpp +// +// The MIT License +// +// Copyright (c) 2015 Wongoo Lee (iwongu at gmail dot com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include + +namespace sqlite3pp +{ + namespace ext + { + + namespace + { + + void function_impl(sqlite3_context* ctx, int nargs, sqlite3_value** values) + { + auto f = static_cast(sqlite3_user_data(ctx)); + context c(ctx, nargs, values); + (*f)(c); + } + + void step_impl(sqlite3_context* ctx, int nargs, sqlite3_value** values) + { + auto p = static_cast*>(sqlite3_user_data(ctx)); + auto s = static_cast((*p).first.get()); + context c(ctx, nargs, values); + ((function::function_handler&)*s)(c); + } + + void finalize_impl(sqlite3_context* ctx) + { + auto p = static_cast*>(sqlite3_user_data(ctx)); + auto f = static_cast((*p).second.get()); + context c(ctx); + ((function::function_handler&)*f)(c); + } + + } // namespace + + + inline context::context(sqlite3_context* ctx, int nargs, sqlite3_value** values) + : ctx_(ctx), nargs_(nargs), values_(values) + { + } + + inline int context::args_count() const + { + return nargs_; + } + + inline int context::args_bytes(int idx) const + { + return sqlite3_value_bytes(values_[idx]); + } + + inline int context::args_type(int idx) const + { + return sqlite3_value_type(values_[idx]); + } + + inline int context::get(int idx, int) const + { + return sqlite3_value_int(values_[idx]); + } + + inline double context::get(int idx, double) const + { + return sqlite3_value_double(values_[idx]); + } + + inline long long int context::get(int idx, long long int) const + { + return sqlite3_value_int64(values_[idx]); + } + + inline char const* context::get(int idx, char const*) const + { + return reinterpret_cast(sqlite3_value_text(values_[idx])); + } + + inline std::string context::get(int idx, std::string) const + { + return get(idx, (char const*)0); + } + + inline void const* context::get(int idx, void const*) const + { + return sqlite3_value_blob(values_[idx]); + } + + + + inline void context::result(int value) + { + sqlite3_result_int(ctx_, value); + } + + inline void context::result(double value) + { + sqlite3_result_double(ctx_, value); + } + + inline void context::result(long long int value) + { + sqlite3_result_int64(ctx_, value); + } + + inline void context::result(std::string const& value) + { + result(value.c_str(), false); + } + + inline void context::result(char const* value, bool fcopy) + { + sqlite3_result_text(ctx_, value, std::strlen(value), fcopy ? SQLITE_TRANSIENT : SQLITE_STATIC); + } + + inline void context::result(void const* value, int n, bool fcopy) + { + sqlite3_result_blob(ctx_, value, n, fcopy ? SQLITE_TRANSIENT : SQLITE_STATIC ); + } + + inline void context::result() + { + sqlite3_result_null(ctx_); + } + + inline void context::result(null_type) + { + sqlite3_result_null(ctx_); + } + + inline void context::result_copy(int idx) + { + sqlite3_result_value(ctx_, values_[idx]); + } + + inline void context::result_error(char const* msg) + { + sqlite3_result_error(ctx_, msg, std::strlen(msg)); + } + + inline void* context::aggregate_data(int size) + { + return sqlite3_aggregate_context(ctx_, size); + } + + inline int context::aggregate_count() + { + return sqlite3_aggregate_count(ctx_); + } + + inline function::function(database& db) : db_(db.db_) + { + } + + inline int function::create(char const* name, function_handler h, int nargs) + { + fh_[name] = pfunction_base(new function_handler(h)); + return sqlite3_create_function(db_, name, nargs, SQLITE_UTF8, fh_[name].get(), function_impl, 0, 0); + } + + inline aggregate::aggregate(database& db) : db_(db.db_) + { + } + + inline int aggregate::create(char const* name, function_handler s, function_handler f, int nargs) + { + ah_[name] = std::make_pair(pfunction_base(new function_handler(s)), pfunction_base(new function_handler(f))); + return sqlite3_create_function(db_, name, nargs, SQLITE_UTF8, &ah_[name], 0, step_impl, finalize_impl); + } + + } // namespace ext + +} // namespace sqlite3pp diff --git a/src/stations/CMakeLists.txt b/src/stations/CMakeLists.txt new file mode 100644 index 0000000..f5652c9 --- /dev/null +++ b/src/stations/CMakeLists.txt @@ -0,0 +1,16 @@ +add_subdirectory(manager) + +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + stations/stationsmatchmodel.h + stations/stationssqlmodel.h + + stations/stationsmatchmodel.cpp + stations/stationssqlmodel.cpp + PARENT_SCOPE +) + +set(TRAINTIMETABLE_UI_FILES + ${TRAINTIMETABLE_UI_FILES} + PARENT_SCOPE +) diff --git a/src/stations/manager/CMakeLists.txt b/src/stations/manager/CMakeLists.txt new file mode 100644 index 0000000..d6f9990 --- /dev/null +++ b/src/stations/manager/CMakeLists.txt @@ -0,0 +1,25 @@ +add_subdirectory(free_rs_viewer) +add_subdirectory(railwaynode) + +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + stations/manager/colordelegate.h + stations/manager/defaultplatfdelegate.h + stations/manager/stationplanmodel.h + stations/manager/stationsmanager.h + stations/manager/stationjobview.h + + stations/manager/colordelegate.cpp + stations/manager/defaultplatfdelegate.cpp + stations/manager/stationplanmodel.cpp + stations/manager/stationsmanager.cpp + stations/manager/stationjobview.cpp + PARENT_SCOPE +) + +set(TRAINTIMETABLE_UI_FILES + ${TRAINTIMETABLE_UI_FILES} + stations/manager/stationsmanager.ui + stations/manager/stationjobview.ui + PARENT_SCOPE +) diff --git a/src/stations/manager/colordelegate.cpp b/src/stations/manager/colordelegate.cpp new file mode 100644 index 0000000..74701d5 --- /dev/null +++ b/src/stations/manager/colordelegate.cpp @@ -0,0 +1,52 @@ +#include "colordelegate.h" + +#include "utils/model_roles.h" + +#include + +#include + +ColorDelegate::ColorDelegate(QObject *parent) : + QStyledItemDelegate(parent) +{ + +} + +void ColorDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QRgb rgb = index.data(COLOR_ROLE).toUInt(); + QColor col(rgb); + painter->fillRect(option.rect, col); +} + +QSize ColorDelegate::sizeHint(const QStyleOptionViewItem &/*option*/, const QModelIndex &/*index*/) const +{ + return QSize(); +} + +QWidget *ColorDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &/*option*/, const QModelIndex &/*index*/) const +{ + QColorDialog *dlg = new QColorDialog(parent); + connect(dlg, &QColorDialog::colorSelected, this, &ColorDelegate::commitAndCloseEditor); + return dlg; +} + +void ColorDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QColorDialog *ed = static_cast(editor); + ed->setCurrentColor(index.data(COLOR_ROLE).toUInt()); +} + +void ColorDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + QColorDialog *ed = static_cast(editor); + model->setData(index, ed->currentColor().rgb(), COLOR_ROLE); +} + +void ColorDelegate::commitAndCloseEditor() +{ + QColorDialog *ed = qobject_cast(sender()); + ed->setEnabled(false); + commitData(ed); + closeEditor(ed); +} diff --git a/src/stations/manager/colordelegate.h b/src/stations/manager/colordelegate.h new file mode 100644 index 0000000..82a728b --- /dev/null +++ b/src/stations/manager/colordelegate.h @@ -0,0 +1,26 @@ +#ifndef COLORDELEGATE_H +#define COLORDELEGATE_H + +#include + +class ColorDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit ColorDelegate(QObject *parent = nullptr); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override; + +private slots: + void commitAndCloseEditor(); +}; + +#endif // COLORDELEGATE_H diff --git a/src/stations/manager/defaultplatfdelegate.cpp b/src/stations/manager/defaultplatfdelegate.cpp new file mode 100644 index 0000000..6a4e9b8 --- /dev/null +++ b/src/stations/manager/defaultplatfdelegate.cpp @@ -0,0 +1,77 @@ +#include "defaultplatfdelegate.h" + +#include "utils/platform_utils.h" + +#include "utils/model_roles.h" + +#include "stations/stationssqlmodel.h" + +DefaultPlatfDelegate::DefaultPlatfDelegate(QObject *parent) : + QStyledItemDelegate(parent) +{ + +} + +QWidget *DefaultPlatfDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &/*option*/, const QModelIndex &index) const +{ + const StationsSQLModel *stModel = qobject_cast(index.model()); + if(!stModel) + return nullptr; + + auto pair = stModel->getPlatfCountAtRow(index.row()); + if(pair.first < 0) + return nullptr; + + PlatformSpinBox *spinBox = new PlatformSpinBox(parent); + spinBox->setRange(-pair.second, pair.first - 1); + + return spinBox; +} + +void DefaultPlatfDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + PlatformSpinBox *spinBox = static_cast(editor); + spinBox->setValue(index.data(PLATF_ID).toInt()); +} + +void DefaultPlatfDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + PlatformSpinBox *spinBox = static_cast(editor); + model->setData(index, spinBox->value(), Qt::EditRole); +} + +PlatformSpinBox::PlatformSpinBox(QWidget *parent) : + QSpinBox(parent) +{ + +} + +QString PlatformSpinBox::textFromValue(int val) const +{ + return utils::shortPlatformName(val); +} + +int PlatformSpinBox::valueFromText(const QString &text) const +{ + int space = text.indexOf(' '); + if(space == -1) + { + bool ok = false; + int platf = text.toInt(&ok); + if(platf > 0) + platf--; //Convert main platf to zero-based + return ok ? platf : value(); + } + + bool ok = false; + int platf = QStringRef(&text, 0, space).toInt(&ok); + if(!ok) + return value(); + + if(QStringRef(&text, space, text.size() - space).startsWith('D', Qt::CaseInsensitive)) + platf = -platf; //Convert to depot + else + platf = qMax(0, platf - 1); //Convert main platf to zero-based + + return platf; +} diff --git a/src/stations/manager/defaultplatfdelegate.h b/src/stations/manager/defaultplatfdelegate.h new file mode 100644 index 0000000..44a3c6c --- /dev/null +++ b/src/stations/manager/defaultplatfdelegate.h @@ -0,0 +1,30 @@ +#ifndef DEFAULTPLATFDELEGATE_H +#define DEFAULTPLATFDELEGATE_H + +#include +#include + +class PlatformSpinBox : public QSpinBox +{ +public: + PlatformSpinBox(QWidget *parent = nullptr); + +protected: + QString textFromValue(int val) const override; + int valueFromText(const QString &text) const override; +}; + +class DefaultPlatfDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit DefaultPlatfDelegate(QObject *parent = nullptr); + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; +}; + +#endif // DEFAULTPLATFDELEGATE_H diff --git a/src/stations/manager/free_rs_viewer/CMakeLists.txt b/src/stations/manager/free_rs_viewer/CMakeLists.txt new file mode 100644 index 0000000..0ea0c87 --- /dev/null +++ b/src/stations/manager/free_rs_viewer/CMakeLists.txt @@ -0,0 +1,9 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + stations/manager/free_rs_viewer/stationfreersmodel.h + stations/manager/free_rs_viewer/stationfreersviewer.h + + stations/manager/free_rs_viewer/stationfreersmodel.cpp + stations/manager/free_rs_viewer/stationfreersviewer.cpp + PARENT_SCOPE +) diff --git a/src/stations/manager/free_rs_viewer/stationfreersmodel.cpp b/src/stations/manager/free_rs_viewer/stationfreersmodel.cpp new file mode 100644 index 0000000..767bf0e --- /dev/null +++ b/src/stations/manager/free_rs_viewer/stationfreersmodel.cpp @@ -0,0 +1,509 @@ +#include "stationfreersmodel.h" + +#include "utils/types.h" +#include "utils/jobcategorystrings.h" +#include "utils/rs_utils.h" + +#include +using namespace sqlite3pp; + +#include + +StationFreeRSModel::StationFreeRSModel(sqlite3pp::database &db, QObject *parent) : + QAbstractTableModel(parent), + sortCol(RSNameCol), + mDb(db) +{ + +} + +QVariant StationFreeRSModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch (section) + { + case RSNameCol: + return tr("Name"); + case FreeFromTimeCol: + return tr("Free from"); + case FreeUpToTimeCol: + return tr("Up to"); + case FromJobCol: + return tr("Job A"); + case ToJobCol: + return tr("Job B"); + } + } + + return QAbstractTableModel::headerData(section, orientation, role); +} + +int StationFreeRSModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_data.size(); +} + +int StationFreeRSModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant StationFreeRSModel::data(const QModelIndex &idx, int role) const +{ + if (role != Qt::DisplayRole || !idx.isValid() || idx.row() >= m_data.size() || idx.column() >= NCols) + return QVariant(); + + const Item& item = m_data.at(idx.row()); + + switch (idx.column()) + { + case RSNameCol: + return item.name; + case FreeFromTimeCol: + return item.from; + case FreeUpToTimeCol: + return item.to; + case FromJobCol: + if(item.fromJob) + return JobCategoryName::jobName(item.fromJob, item.fromJobCat); + break; + case ToJobCol: + if(item.toJob) + return JobCategoryName::jobName(item.toJob, item.toJobCat); + break; + } + + return QVariant(); +} + +const StationFreeRSModel::Item* StationFreeRSModel::getItemAt(int row) const +{ + if(row < m_data.size()) + return &m_data.at(row); + return nullptr; +} + +void StationFreeRSModel::setStation(db_id stId) +{ + m_stationId = stId; + reloadData(); +} + +void StationFreeRSModel::setTime(QTime time) +{ + m_time = time; + reloadData(); +} + +void StationFreeRSModel::reloadData() +{ + beginResetModel(); + + m_data.clear(); + QHash tempLookup; + + query q(mDb, "SELECT coupling.rsId,rs_list.number,rs_models.name,rs_models.suffix,rs_models.type," + "MAX(stops.arrival),stops.jobId,jobs.category,stops.id" + " FROM coupling" + " JOIN stops ON stops.id=coupling.stopId" + " JOIN jobs ON jobs.id=stops.jobId" + " JOIN rs_list ON rs_list.id=coupling.rsId" + " LEFT JOIN rs_models ON rs_models.id=rs_list.model_id" + " WHERE stops.stationId=? AND stops.arrival<=?" //Less than OR equal (this includes RS uncoupled exactly at that time) + " GROUP BY coupling.rsId" + " HAVING coupling.operation=0"); + q.bind(1, m_stationId); + q.bind(2, m_time); + + //Select uncoupled before m_time + for(auto r : q) + { + Item item; + 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)); + RsType type = RsType(sqlite3_column_int(q.stmt(), 4)); + + item.from = r.get(5); + item.fromJob = r.get(6); + item.fromJobCat = JobCategory(r.get(7)); + item.fromStopId = r.get(8); + item.name = rs_utils::formatNameRef(modelName, modelNameLen, number, modelSuffix, modelSuffixLen, type); + + tempLookup.insert(item.rsId, item); + } + + q.prepare("SELECT coupling.rsId,rs_list.number,rs_models.name,rs_models.suffix,rs_models.type," + "MIN(stops.arrival),stops.jobId,jobs.category,stops.id" + " FROM coupling" + " JOIN stops ON stops.id=coupling.stopId" + " JOIN jobs ON jobs.id=stops.jobId" + " JOIN rs_list ON rs_list.id=coupling.rsId" + " LEFT JOIN rs_models ON rs_models.id=rs_list.model_id" + " WHERE stops.stationId=? AND stops.arrival>?" //Greater than NOT equal (this exclude RS coupled at exactly that time) + " GROUP BY coupling.rsId" + " HAVING coupling.operation=1"); + q.bind(1, m_stationId); + q.bind(2, m_time); + + //Select coupled after m_time + for(auto r : q) + { + db_id rsId = r.get(0); + Item& item = tempLookup[rsId]; //Create entry if key doesn't exist + if(!item.rsId) + { + item.rsId = rsId; + + 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)); + RsType type = RsType(sqlite3_column_int(q.stmt(), 4)); + + item.name = rs_utils::formatNameRef(modelName, modelNameLen, number, modelSuffix, modelSuffixLen, type); + } + item.to = r.get(5); + item.toJob = r.get(6); + item.toJobCat = JobCategory(r.get(7)); + item.toStopId = r.get(8); + } + + m_data.reserve(tempLookup.size()); + + //Insert and sort + switch (sortCol) + { + default: + case RSNameCol: + { + for(auto it : qAsConst(tempLookup)) + { + std::ptrdiff_t len = m_data.size(); + QVector::iterator first = m_data.begin(); + + while (len > 0) + { + std::ptrdiff_t half = len >> 1; + QVector::iterator middle = first + half; + if (it.name < middle->name) + len = half; + else + { + first = middle; + ++first; + len = len - half - 1; + } + } + + m_data.insert(first, it); + } + break; + } + case FreeFromTimeCol: + { + for(auto it : qAsConst(tempLookup)) + { + std::ptrdiff_t len = m_data.size(); + QVector::iterator first = m_data.begin(); + + while (len > 0) + { + std::ptrdiff_t half = len >> 1; + QVector::iterator middle = first + half; + if (it.from < middle->from || (it.from == middle->from && it.name < middle->name)) + len = half; + else + { + first = middle; + ++first; + len = len - half - 1; + } + } + + m_data.insert(first, it); + } + break; + } + case FreeUpToTimeCol: + { + for(auto it : qAsConst(tempLookup)) + { + std::ptrdiff_t len = m_data.size(); + QVector::iterator first = m_data.begin(); + + while (len > 0) + { + std::ptrdiff_t half = len >> 1; + QVector::iterator middle = first + half; + if (it.to < middle->to || (it.to == middle->to && it.name < middle->name)) + len = half; + else + { + first = middle; + ++first; + len = len - half - 1; + } + } + + m_data.insert(first, it); + } + break; + } + case FromJobCol: + { + for(auto it : qAsConst(tempLookup)) + { + std::ptrdiff_t len = m_data.size(); + QVector::iterator first = m_data.begin(); + + while (len > 0) + { + std::ptrdiff_t half = len >> 1; + QVector::iterator middle = first + half; + if (it.fromJob < middle->fromJob || (it.fromJob == middle->fromJob && it.name < middle->name)) + len = half; + else + { + first = middle; + ++first; + len = len - half - 1; + } + } + + m_data.insert(first, it); + } + break; + } + case ToJobCol: + { + for(auto it : qAsConst(tempLookup)) + { + std::ptrdiff_t len = m_data.size(); + QVector::iterator first = m_data.begin(); + + while (len > 0) + { + std::ptrdiff_t half = len >> 1; + QVector::iterator middle = first + half; + if (it.toJob < middle->toJob || (it.toJob == middle->toJob && it.name < middle->name)) + len = half; + else + { + first = middle; + ++first; + len = len - half - 1; + } + } + + m_data.insert(first, it); + } + break; + } + } + + m_data.squeeze(); + + endResetModel(); +} + +int StationFreeRSModel::getSortCol() const +{ + return sortCol; +} + +db_id StationFreeRSModel::getStationId() const +{ + return m_stationId; +} + +QString StationFreeRSModel::getStationName() const +{ + query q(mDb, "SELECT name FROM stations WHERE id=?"); + q.bind(1, m_stationId); + q.step(); + return q.getRows().get(0); +} + +QTime StationFreeRSModel::getTime() const +{ + return m_time; +} + +StationFreeRSModel::ErrorCodes StationFreeRSModel::getNextOpTime(QTime &time) +{ + ErrorCodes err = NoError; + + query q_getNextOpTime(mDb, "SELECT coupling.rsId, MIN(stops.arrival) FROM coupling" + " JOIN stops ON stops.id=coupling.stopId" + " WHERE stops.stationId=? AND stops.arrival>?"); + q_getNextOpTime.bind(1, m_stationId); + q_getNextOpTime.bind(2, m_time); + + if(q_getNextOpTime.step() == SQLITE_ROW) + { + auto r = q_getNextOpTime.getRows(); + if(r.column_type(1) != SQLITE_NULL) + { + time = r.get(1); + } + else + { + //There aren't operations next to m_time + err = NoOperationFound; + } + } + else + { + //Error + qDebug() << __PRETTY_FUNCTION__ << "DB Error:" << mDb.error_code() << mDb.error_msg() << mDb.extended_error_code(); + err = DBError; + } + + q_getNextOpTime.reset(); + return err; +} + +StationFreeRSModel::ErrorCodes StationFreeRSModel::getPrevOpTime(QTime &time) +{ + //TODO: if on last/first operation increment by 1 to see after/before prev, last operation + ErrorCodes err = NoError; + + query q_getPrevOpTime(mDb, "SELECT coupling.rsId, MAX(stops.arrival) FROM coupling" + " JOIN stops ON stops.id=coupling.stopId" + " WHERE stops.stationId=? AND stops.arrival(1); + } + else + { + //There aren't operations previous to m_time + //But because RS uncoupled before m_time are taken with 'less than OR equal' + //we should show also the situation before this time. + + //err = NoOperationFound; + + //Fake operation at first morning + time = QTime(0, 0); + } + } + else + { + //Error + qDebug() << __PRETTY_FUNCTION__ << "DB Error:" << mDb.error_code() << mDb.error_msg() << mDb.extended_error_code(); + err = DBError; + } + + q_getPrevOpTime.reset(); + return err; +} + +bool StationFreeRSModel::sortByColumn(int col) +{ + if(col == sortCol || col < 0 || col >= NCols) + return false; + + sortCol = col; + + beginResetModel(); + switch (sortCol) + { + default: + case RSNameCol: + { + class CompName : public std::binary_function + { + public: + inline bool operator()(const Item& l, const Item& r) + { + return l.name < r.name; + } + }; + + std::sort(m_data.begin(), m_data.end(), CompName()); + break; + } + case FreeFromTimeCol: + { + class CompFrom : public std::binary_function + { + public: + inline bool operator()(const Item& l, const Item& r) + { + if(l.from == r.from) + return l.name < r.name; + return l.from < r.from; + } + }; + + std::sort(m_data.begin(), m_data.end(), CompFrom()); + break; + } + case FreeUpToTimeCol: + { + class CompTo : public std::binary_function + { + public: + inline bool operator()(const Item& l, const Item& r) + { + if(l.to == r.to) + return l.name < r.name; + return l.to < r.to; + } + }; + + std::sort(m_data.begin(), m_data.end(), CompTo()); + break; + } + case FromJobCol: + { + class CompJobFrom : public std::binary_function + { + public: + inline bool operator()(const Item& l, const Item& r) + { + if(l.fromJob == r.fromJob) + return l.name < r.name; + return l.fromJob < r.fromJob; + } + }; + + std::sort(m_data.begin(), m_data.end(), CompJobFrom()); + break; + } + case ToJobCol: + { + class CompJobTo : public std::binary_function + { + public: + inline bool operator()(const Item& l, const Item& r) + { + if(l.toJob == r.toJob) + return l.name < r.name; + return l.toJob < r.toJob; + } + }; + + std::sort(m_data.begin(), m_data.end(), CompJobTo()); + break; + } + } + endResetModel(); + + return true; +} diff --git a/src/stations/manager/free_rs_viewer/stationfreersmodel.h b/src/stations/manager/free_rs_viewer/stationfreersmodel.h new file mode 100644 index 0000000..4965289 --- /dev/null +++ b/src/stations/manager/free_rs_viewer/stationfreersmodel.h @@ -0,0 +1,91 @@ +#ifndef STATIONFREERSMODEL_H +#define STATIONFREERSMODEL_H + +#include + +#include + +#include + +#include + +#include "utils/types.h" + +//TODO: on-demand load and let SQL do the sorting +class StationFreeRSModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + enum Columns { + RSNameCol = 0, + FreeFromTimeCol, + FreeUpToTimeCol, + FromJobCol, + ToJobCol, + NCols + }; + + typedef struct Item_ + { + db_id rsId = 0; + QTime from; //Time at which RS is uncoupled (from now it's free) + QTime to; //Time at which is coupled (not free anymore) + QString name; + db_id fromJob = 0; + db_id fromStopId = 0; + db_id toJob = 0; + db_id toStopId = 0; + JobCategory fromJobCat; + JobCategory toJobCat; + } Item; + + StationFreeRSModel(sqlite3pp::database &db, QObject *parent = nullptr); + + // 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; + + const Item *getItemAt(int row) const; + + void setStation(db_id stId); + void setTime(QTime time); + + enum ErrorCodes + { + NoError = 0, + NoOperationFound, + DBError + }; + + ErrorCodes getNextOpTime(QTime &time); + ErrorCodes getPrevOpTime(QTime& time); + + QTime getTime() const; + db_id getStationId() const; + + QString getStationName() const; + + bool sortByColumn(int col); + + int getSortCol() const; + +public slots: + void reloadData(); + +private: + db_id m_stationId; + QTime m_time; + int sortCol; + + QVector m_data; + + sqlite3pp::database& mDb; +}; + +#endif // STATIONFREERSMODEL_H diff --git a/src/stations/manager/free_rs_viewer/stationfreersviewer.cpp b/src/stations/manager/free_rs_viewer/stationfreersviewer.cpp new file mode 100644 index 0000000..875777b --- /dev/null +++ b/src/stations/manager/free_rs_viewer/stationfreersviewer.cpp @@ -0,0 +1,188 @@ +#include "stationfreersviewer.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "app/session.h" +#include "viewmanager/viewmanager.h" + +#include "stationfreersmodel.h" + +/* Widget to view in a list all free rollingstock pieces at a given time m_time + * in a given time m_stationId +*/ +StationFreeRSViewer::StationFreeRSViewer(QWidget *parent) : + QWidget(parent) +{ + QGridLayout *lay = new QGridLayout(this); + + refreshBut = new QPushButton(tr("Refresh")); + lay->addWidget(refreshBut, 0, 0, 1, 2); + + QLabel *l = new QLabel(tr("Time:")); + lay->addWidget(l, 1, 0); + + timeEdit = new QTimeEdit; + lay->addWidget(timeEdit, 1, 1); + + prevOpBut = new QPushButton(tr("Previous Operation")); + lay->addWidget(prevOpBut, 2, 0); + + nextOpBut = new QPushButton(tr("Next Operation")); + lay->addWidget(nextOpBut, 2, 1); + + view = new QTableView; + view->setContextMenuPolicy(Qt::CustomContextMenu); + view->setSelectionBehavior(QTableView::SelectRows); + lay->addWidget(view, 3, 0, 1, 2); + + model = new StationFreeRSModel(Session->m_Db, this); + view->setModel(model); + + connect(refreshBut, &QPushButton::clicked, model, &StationFreeRSModel::reloadData); + connect(timeEdit, &QTimeEdit::editingFinished, this, &StationFreeRSViewer::onTimeEditingFinished); + + connect(nextOpBut, &QPushButton::clicked, this, &StationFreeRSViewer::goToNext); + connect(prevOpBut, &QPushButton::clicked, this, &StationFreeRSViewer::goToPrev); + + connect(view, &QTableView::customContextMenuRequested, this, &StationFreeRSViewer::showContextMenu); + + //Custom colun sorting + //NOTE: leave disconnect() in the old SIGLAL()/SLOT() version in order to work + QHeaderView *header = view->horizontalHeader(); + disconnect(header, SIGNAL(sectionPressed(int)), view, SLOT(selectColumn(int))); + disconnect(header, SIGNAL(sectionEntered(int)), view, SLOT(_q_selectColumn(int))); + + connect(header, &QHeaderView::sectionClicked, this, &StationFreeRSViewer::sectionClicked); + header->setSortIndicatorShown(true); + header->setSortIndicator(StationFreeRSModel::RSNameCol, Qt::AscendingOrder); + + header->setSectionResizeMode(StationFreeRSModel::FreeFromTimeCol, QHeaderView::ResizeToContents); + + setMinimumSize(100, 200); +} + +void StationFreeRSViewer::setStation(db_id stId) +{ + model->setStation(stId); + updateTitle(); +} + +void StationFreeRSViewer::updateTitle() +{ + setWindowTitle(tr("Free Rollingstock in %1").arg(model->getStationName())); +} + +void StationFreeRSViewer::updateData() +{ + model->reloadData(); +} + +void StationFreeRSViewer::onTimeEditingFinished() +{ + model->setTime(timeEdit->time()); +} + +/* void StationFreeRSViewer::goToNext() + * Find the first operation after m_time. + * If found set m_time to this new value and rebuild the list. + * This is useful to jump between operation withuot having to guess times. +*/ +void StationFreeRSViewer::goToNext() +{ + QTime time; + StationFreeRSModel::ErrorCodes err = model->getNextOpTime(time); + + if(err == StationFreeRSModel::DBError) + { + QMessageBox::warning(this, tr("Error"), + tr("Database error. Try again.")); + return; + } + + if(err == StationFreeRSModel::NoOperationFound) + { + QMessageBox::information(this, tr("No Operation Found"), + tr("No operation found in station %1 after %2!") + .arg(model->getStationName()) + .arg(model->getTime().toString("HH:mm"))); + return; + } + + timeEdit->setTime(time); + model->setTime(time); +} + +/* void StationFreeRSViewer::goToPrev() + * Find the last operation before m_time. + * If found set m_time to this new value and rebuild the list. + * This is useful to jump between operation withuot having to guess times. + * See 'void StationFreeRSViewer::goToNext()' +*/ +void StationFreeRSViewer::goToPrev() +{ + QTime time; + StationFreeRSModel::ErrorCodes err = model->getPrevOpTime(time); + + if(err == StationFreeRSModel::DBError) + { + QMessageBox::warning(this, tr("Error"), + tr("Database error. Try again.")); + return; + } + + if(err == StationFreeRSModel::NoOperationFound) + { + QMessageBox::information(this, tr("No Operation Found"), + tr("No operation found in station %1 before %2!") + .arg(model->getStationName()) + .arg(model->getTime().toString("HH:mm"))); + return; + } + + timeEdit->setTime(time); + model->setTime(time); +} + +void StationFreeRSViewer::showContextMenu(const QPoint& pos) +{ + QModelIndex idx = view->indexAt(pos); + if(!idx.isValid()) + return; + + const StationFreeRSModel::Item *item = model->getItemAt(idx.row()); + + QMenu menu(this); + QAction *showRSPlan = menu.addAction(tr("Show RS Plan")); + QAction *showFromJobInEditor = menu.addAction(tr("Show Job A in JobEditor")); + QAction *showToJobInEditor = menu.addAction(tr("Show Job B in JobEditor")); + + showFromJobInEditor->setEnabled(item->fromJob); + showToJobInEditor->setEnabled(item->toJob); + + QAction *act = menu.exec(view->viewport()->mapToGlobal(pos)); + if(act == showRSPlan) + { + Session->getViewManager()->requestRSInfo(item->rsId); + } + else if(act == showFromJobInEditor) + { + Session->getViewManager()->requestJobEditor(item->fromJob, item->fromStopId); + } + else if(act == showToJobInEditor) + { + Session->getViewManager()->requestJobEditor(item->toJob, item->toStopId); + } +} + +void StationFreeRSViewer::sectionClicked(int col) +{ + model->sortByColumn(col); + view->horizontalHeader()->setSortIndicator(model->getSortCol(), Qt::AscendingOrder); +} diff --git a/src/stations/manager/free_rs_viewer/stationfreersviewer.h b/src/stations/manager/free_rs_viewer/stationfreersviewer.h new file mode 100644 index 0000000..16d91d0 --- /dev/null +++ b/src/stations/manager/free_rs_viewer/stationfreersviewer.h @@ -0,0 +1,44 @@ +#ifndef STATIONFREERSVIEWER_H +#define STATIONFREERSVIEWER_H + +#include + +#include "utils/types.h" + +class StationFreeRSModel; +class QTimeEdit; +class QTableView; +class QPushButton; + +class StationFreeRSViewer : public QWidget +{ + Q_OBJECT +public: + explicit StationFreeRSViewer(QWidget *parent = nullptr); + + void setStation(db_id stId); + void updateTitle(); + void updateData(); + +public slots: + void goToNext(); + void goToPrev(); + +private slots: + void onTimeEditingFinished(); + + void showContextMenu(const QPoint &pos); + + void sectionClicked(int col); + +private: + StationFreeRSModel *model; + QTableView *view; + + QTimeEdit *timeEdit; + QPushButton *refreshBut; + QPushButton *nextOpBut; + QPushButton *prevOpBut; +}; + +#endif // STATIONFREERSVIEWER_H diff --git a/src/stations/manager/railwaynode/CMakeLists.txt b/src/stations/manager/railwaynode/CMakeLists.txt new file mode 100644 index 0000000..44cb156 --- /dev/null +++ b/src/stations/manager/railwaynode/CMakeLists.txt @@ -0,0 +1,16 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + stations/manager/railwaynode/kmdelegate.h + stations/manager/railwaynode/kmspinbox.h + stations/manager/railwaynode/railwaynodeeditor.h + stations/manager/railwaynode/railwaynodemode.h + stations/manager/railwaynode/railwaynodemodel.h + stations/manager/railwaynode/stationorlinematchfactory.h + + stations/manager/railwaynode/kmdelegate.cpp + stations/manager/railwaynode/kmspinbox.cpp + stations/manager/railwaynode/railwaynodeeditor.cpp + stations/manager/railwaynode/railwaynodemodel.cpp + stations/manager/railwaynode/stationorlinematchfactory.cpp + PARENT_SCOPE +) diff --git a/src/stations/manager/railwaynode/kmdelegate.cpp b/src/stations/manager/railwaynode/kmdelegate.cpp new file mode 100644 index 0000000..c8afd5b --- /dev/null +++ b/src/stations/manager/railwaynode/kmdelegate.cpp @@ -0,0 +1,31 @@ +#include "kmdelegate.h" +#include "kmspinbox.h" + +KmDelegate::KmDelegate(QObject *parent) : + QStyledItemDelegate(parent) +{ + +} + +QWidget *KmDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &/*option*/, const QModelIndex &/*index*/) const +{ + KmSpinBox *spin = new KmSpinBox(parent); + return spin; +} + +void KmDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + KmSpinBox *spin = static_cast(editor); + spin->setValue(index.data(Qt::EditRole).toInt()); +} + +void KmDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + KmSpinBox *spin = static_cast(editor); + model->setData(index, spin->value(), Qt::EditRole); +} + +void KmDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &/*index*/) const +{ + editor->setGeometry(option.rect); +} diff --git a/src/stations/manager/railwaynode/kmdelegate.h b/src/stations/manager/railwaynode/kmdelegate.h new file mode 100644 index 0000000..1d66eee --- /dev/null +++ b/src/stations/manager/railwaynode/kmdelegate.h @@ -0,0 +1,23 @@ +#ifndef KMDELEGATE_H +#define KMDELEGATE_H + +#include + +class KmDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + KmDelegate(QObject *parent = nullptr); + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override; + + void updateEditorGeometry(QWidget *editor, + const QStyleOptionViewItem &option, const QModelIndex &index) const override; +}; + +#endif // KMDELEGATE_H diff --git a/src/stations/manager/railwaynode/kmspinbox.cpp b/src/stations/manager/railwaynode/kmspinbox.cpp new file mode 100644 index 0000000..dd42229 --- /dev/null +++ b/src/stations/manager/railwaynode/kmspinbox.cpp @@ -0,0 +1,182 @@ +#include "kmspinbox.h" +#include "utils/kmutils.h" + +#include + +#include + +#include + +KmSpinBox::KmSpinBox(QWidget *parent) : + QSpinBox(parent), + currentSection(KmSection) +{ + setRange(0, 9999 * 1000 + 999); //9999 km + 999 meters + setAlignment(Qt::AlignRight | Qt::AlignVCenter); + connect(lineEdit(), &QLineEdit::cursorPositionChanged, this, &KmSpinBox::cursorPosChanged); +} + +void KmSpinBox::focusInEvent(QFocusEvent *e) +{ + QSpinBox::focusInEvent(e); + setCurrentSection(KmSection); +} + +void KmSpinBox::keyPressEvent(QKeyEvent *e) +{ + bool plusPressed = false; + if(e->key() == Qt::Key_Plus || e->text().contains('+')) + plusPressed = true; + QSpinBox::keyPressEvent(e); + if(plusPressed && currentSection == KmSection) + setCurrentSection(MetersSection); +} + +bool KmSpinBox::focusNextPrevChild(bool next) +{ + if((currentSection == MetersSection) == next) + { + //Before KmSection or after MetersSection is out of the widget + return QSpinBox::focusNextPrevChild(next); + } + setCurrentSection(currentSection == KmSection ? MetersSection : KmSection); + return true; +} + +QValidator::State KmSpinBox::validate(QString &input, int &pos) const +{ + int plusPos = -1; + int val = 0; + int firstNonBlank = 0; + int i = 0; + bool empty = true; + for(; i < input.size(); i++) + { + QChar ch = input.at(i); + if(ch.isSpace()) + { + if(!empty) + break; + firstNonBlank++; + continue; + } + if(ch.isDigit()) + { + val *= 10; + val += ch.digitValue(); + empty = false; + continue; + } + if(ch != '+' || plusPos != -1) //Not '+' or digit or multiple '+' + return QValidator::Invalid; + plusPos = i; + } + + if(empty || val > maximum() || plusPos == -1) + return QValidator::Invalid; + + if(plusPos != i - 4) + return QValidator::Intermediate; //+ is not 3 chars from last + + if(firstNonBlank > 0 && input.at(firstNonBlank) == '+') + firstNonBlank--; + pos -= input.size() - i - firstNonBlank; + input = input.mid(firstNonBlank, i - firstNonBlank); + if(input.at(0) == '+') + input.prepend('0'); //Add leading zero + else if(input.size() > 1 && input.at(1) == '+' && !input.at(0).isDigit()) + input[0] = '0'; + + return QValidator::Acceptable; +} + +int KmSpinBox::valueFromText(const QString &text) const +{ + return utils::kmNumFromTextInMeters(text); +} + +QString KmSpinBox::textFromValue(int val) const +{ + return utils::kmNumToText(val); +} + +void KmSpinBox::fixup(QString &str) const +{ + if(str.isEmpty() || str.at(0) == '+') + str.prepend('0'); + int plusPos = str.indexOf('+'); + if(plusPos < 0) + { + str.append('+'); + plusPos = str.size() - 1; + } + else if(str.indexOf('+', plusPos + 1) >= 0) + { + return; //Do not fix if there are multiple '+' + } + + int nDigitsAfterPlus = str.size() - plusPos - 1; + if(nDigitsAfterPlus < 3) + { + for(int i = nDigitsAfterPlus; i < 3; i++) + { + str.append('0'); + } + } + else if(nDigitsAfterPlus > 3) + { + str.chop(nDigitsAfterPlus - 3); + } +} + +void KmSpinBox::stepBy(int steps) +{ + int val = value(); + if(currentSection == KmSection) + { + //If steps are negative apply only if result km part is >= 0 + val += steps * 1000; + if(val >= 0) + { + setValue(val); + } + } + else + { + //Keep 0 <= meters <= 999 otherwhise we change also km part + int meters = val % 1000 + steps; + if(meters >= 0 && meters <= 999) + setValue(val + steps); + } + setCurrentSection(currentSection); +} + +void KmSpinBox::cursorPosChanged(int /*oldPos*/, int newPos) +{ + QString text = lineEdit()->text(); + int plusPos = text.indexOf('+'); + if(plusPos < 0) + { + currentSection = KmSection; + return; + } + currentSection = newPos <= plusPos ? KmSection : MetersSection; +} + +void KmSpinBox::setCurrentSection(int section) +{ + QLineEdit *edit = lineEdit(); + QString text = edit->text(); + int plusPos = text.indexOf('+'); + if(plusPos < 0) + { + currentSection = KmSection; + return; + } + edit->deselect(); + currentSection = section; + if(currentSection == KmSection) + edit->setSelection(0, plusPos); + else + edit->setSelection(plusPos + 1, text.size() - plusPos - 1); +} diff --git a/src/stations/manager/railwaynode/kmspinbox.h b/src/stations/manager/railwaynode/kmspinbox.h new file mode 100644 index 0000000..6422138 --- /dev/null +++ b/src/stations/manager/railwaynode/kmspinbox.h @@ -0,0 +1,40 @@ +#ifndef KMSPINBOX_H +#define KMSPINBOX_H + +#include + +class KmSpinBox : public QSpinBox +{ + Q_OBJECT +public: + explicit KmSpinBox(QWidget *parent = nullptr); + + enum Sections + { + KmSection = 0, + MetersSection + }; + + void stepBy(int steps) override; + +protected: + QValidator::State validate(QString &input, int &pos) const override; + int valueFromText(const QString &text) const override; + QString textFromValue(int val) const override; + void fixup(QString &str) const override; + + bool focusNextPrevChild(bool next) override; + void focusInEvent(QFocusEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + +private slots: + void cursorPosChanged(int oldPos, int newPos); + +private: + void setCurrentSection(int section); + +private: + int currentSection; +}; + +#endif // KMSPINBOX_H diff --git a/src/stations/manager/railwaynode/railwaynodeeditor.cpp b/src/stations/manager/railwaynode/railwaynodeeditor.cpp new file mode 100644 index 0000000..e304429 --- /dev/null +++ b/src/stations/manager/railwaynode/railwaynodeeditor.cpp @@ -0,0 +1,136 @@ +#include "railwaynodeeditor.h" + +#include "railwaynodemodel.h" + +#include "utils/combodelegate.h" +#include "kmdelegate.h" + +#include "utils/sqldelegate/isqlfkmatchmodel.h" +#include "utils/sqldelegate/sqlfkfielddelegate.h" +#include "stationorlinematchfactory.h" + +#include "utils/model_roles.h" + +#include +#include +#include +#include +#include +#include + +#include "utils/sqldelegate/modelpageswitcher.h" + +#include "utils/sqldelegate/chooseitemdlg.h" + +RailwayNodeEditor::RailwayNodeEditor(sqlite3pp::database &db, QWidget *parent) : + QDialog(parent) +{ + QVBoxLayout *l = new QVBoxLayout(this); + toolBar = new QToolBar(this); + l->addWidget(toolBar); + + toolBar->addAction(tr("Add"), this, &RailwayNodeEditor::addItem); + toolBar->addAction(tr("Remove"), this, &RailwayNodeEditor::removeCurrentItem); + + view = new QTableView(this); + l->addWidget(view); + + auto ps = new ModelPageSwitcher(false, this); + l->addWidget(ps); + + model = new RailwayNodeModel(db, this); + view->setModel(model); + ps->setModel(model); + + //Custom colun sorting + //NOTE: leave disconnect() in the old SIGLAL()/SLOT() version in order to work + QHeaderView *header = view->horizontalHeader(); + disconnect(header, SIGNAL(sectionPressed(int)), view, SLOT(selectColumn(int))); + disconnect(header, SIGNAL(sectionEntered(int)), view, SLOT(_q_selectColumn(int))); + connect(header, &QHeaderView::sectionClicked, this, [this, header](int section) + { + model->setSortingColumn(section); + header->setSortIndicator(model->getSortingColumn(), Qt::AscendingOrder); + }); + header->setSortIndicatorShown(true); + header->setSortIndicator(model->getSortingColumn(), Qt::AscendingOrder); + + factory = new StationOrLineMatchFactory(db, this); + lineOrStationDelegate = new SqlFKFieldDelegate(factory, model, this); + view->setItemDelegateForColumn(RailwayNodeModel::LineOrStationCol, lineOrStationDelegate); + + KmDelegate *kmDelegate = new KmDelegate(this); + view->setItemDelegateForColumn(RailwayNodeModel::KmCol, kmDelegate); + + QStringList directionNames; + const int size = sizeof (DirectionNamesTable)/sizeof (DirectionNamesTable[0]); + directionNames.reserve(size); + for(int i = 0; i < size; i++) + { + directionNames.append(DirectionNames::tr(DirectionNamesTable[i])); + } + directionDelegate = new ComboDelegate(directionNames, DIRECTION_ROLE, this); + view->setItemDelegateForColumn(RailwayNodeModel::DirectionCol, directionDelegate); + + QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Ok, this); + connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept); + l->addWidget(box); + + setMinimumSize(500, 300); +} + +void RailwayNodeEditor::setMode(const QString& name, db_id id, RailwayNodeMode mode) +{ + factory->setMode(mode); + model->setMode(id, mode); + setWindowTitle(name); +} + +void RailwayNodeEditor::addItem() +{ + ISqlFKMatchModel *matchModel = factory->createModel(); + ChooseItemDlg dlg(matchModel, this); + matchModel->setParent(&dlg); + if(model->getMode() == RailwayNodeMode::StationLinesMode) + { + dlg.setDescription(tr("Please choose a line for the new entry")); + dlg.setPlaceholder(tr("Line")); + }else{ + dlg.setDescription(tr("Please choose a station for the new entry")); + dlg.setPlaceholder(tr("Station")); + } + dlg.setCallback([this](db_id itemId, QString &errMsg) -> bool + { + if(!itemId) + { + if(model->getMode() == RailwayNodeMode::StationLinesMode) + errMsg = tr("You must select a valid line."); + else + errMsg = tr("You must select a valid station."); + return false; + } + + if(!model->addItem(itemId, nullptr)) + { + if(model->getMode() == RailwayNodeMode::StationLinesMode) + errMsg = tr("You cannot add the same line twice to the same station."); + else + errMsg = tr("You cannot add the same station twice to the same line."); + return false; + } + return true; + }); + + if(dlg.exec() == QDialog::Accepted) + { + //TODO: select and edit the new item + } +} + +void RailwayNodeEditor::removeCurrentItem() +{ + if(!view->selectionModel()->hasSelection()) + return; + int row = view->currentIndex().row(); + model->removeItem(row); +} diff --git a/src/stations/manager/railwaynode/railwaynodeeditor.h b/src/stations/manager/railwaynode/railwaynodeeditor.h new file mode 100644 index 0000000..e21e284 --- /dev/null +++ b/src/stations/manager/railwaynode/railwaynodeeditor.h @@ -0,0 +1,41 @@ +#ifndef RAILWAYNODEEDITOR_H +#define RAILWAYNODEEDITOR_H + +#include + +#include "utils/types.h" +#include "railwaynodemode.h" + +class QToolBar; +class QTableView; +class RailwayNodeModel; +class SqlFKFieldDelegate; +class StationOrLineMatchFactory; +class ComboDelegate; + +namespace sqlite3pp { +class database; +} + +class RailwayNodeEditor : public QDialog +{ + Q_OBJECT +public: + RailwayNodeEditor(sqlite3pp::database &db, QWidget *parent = nullptr); + + void setMode(const QString &name, db_id id, RailwayNodeMode mode); + +public slots: + void addItem(); + void removeCurrentItem(); + +private: + QToolBar *toolBar; + QTableView *view; + RailwayNodeModel *model; + SqlFKFieldDelegate *lineOrStationDelegate; + StationOrLineMatchFactory *factory; + ComboDelegate *directionDelegate; +}; + +#endif // RAILWAYNODEEDITOR_H diff --git a/src/stations/manager/railwaynode/railwaynodemode.h b/src/stations/manager/railwaynode/railwaynodemode.h new file mode 100644 index 0000000..094c30f --- /dev/null +++ b/src/stations/manager/railwaynode/railwaynodemode.h @@ -0,0 +1,10 @@ +#ifndef RAILWAYNODEMODE_H +#define RAILWAYNODEMODE_H + +enum class RailwayNodeMode +{ + StationLinesMode, + LineStationsMode +}; + +#endif // RAILWAYNODEMODE_H diff --git a/src/stations/manager/railwaynode/railwaynodemodel.cpp b/src/stations/manager/railwaynode/railwaynodemodel.cpp new file mode 100644 index 0000000..bffa5e6 --- /dev/null +++ b/src/stations/manager/railwaynode/railwaynodemodel.cpp @@ -0,0 +1,836 @@ +#include "railwaynodemodel.h" + +#include "app/session.h" //TODO: needed? +#include "lines/linestorage.h" + +#include +#include +#include "utils/worker_event_types.h" + +#include +using namespace sqlite3pp; + +#include "utils/model_roles.h" +#include "utils/kmutils.h" + +#include + +class RailwayNodeModelResultEvent : public QEvent +{ +public: + static constexpr Type _Type = Type(CustomEvents::RailwayNodeModelResult); + inline RailwayNodeModelResultEvent() :QEvent(_Type) {} + + QVector items; + int firstRow; +}; + +//ERRORMSG: show other errors +static constexpr char +errorRSInUseCannotDelete[] = QT_TRANSLATE_NOOP("RailwayNodeModel", + "Rollingstock item %1 is used in some jobs so it cannot be removed.\n" + "If you wish to remove it, please first remove it from its jobs."); + +RailwayNodeModel::RailwayNodeModel(sqlite3pp::database &db, QObject *parent) : + IPagedItemModel(500, db, parent), + cacheFirstRow(0), + firstPendingRow(-BatchSize), + stationOrLineId(0), + m_mode(RailwayNodeMode::StationLinesMode) +{ + sortColumn = KmCol; +} + +bool RailwayNodeModel::event(QEvent *e) +{ + if(e->type() == RailwayNodeModelResultEvent::_Type) + { + RailwayNodeModelResultEvent *ev = static_cast(e); + ev->setAccepted(true); + + handleResult(ev->items, ev->firstRow); + + return true; + } + + return QAbstractTableModel::event(e); +} + +QVariant RailwayNodeModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(role == Qt::DisplayRole) + { + if(orientation == Qt::Horizontal) + { + switch (section) + { + case LineOrStationCol: + return m_mode == RailwayNodeMode::StationLinesMode ? tr("Line") : tr("Station"); + case KmCol: + return tr("Km"); + case DirectionCol: + return tr("Direction"); + default: + break; + } + } + else + { + return section + curPage * ItemsPerPage + 1; + } + } + return IPagedItemModel::headerData(section, orientation, role); +} + +int RailwayNodeModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : curItemCount; +} + +int RailwayNodeModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant RailwayNodeModel::data(const QModelIndex &idx, int role) const +{ + const int row = idx.row(); + if (!idx.isValid() || row >= curItemCount || idx.column() >= NCols) + return QVariant(); + + 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 Item& item = cache.at(row - cacheFirstRow); + + switch (role) + { + case Qt::DisplayRole: + { + switch (idx.column()) + { + case LineOrStationCol: + return item.lineOrStationName; + case KmCol: + return utils::kmNumToText(item.kmInMeters); + case DirectionCol: + return DirectionNames::name(item.direction); + } + break; + } + case Qt::EditRole: + { + switch (idx.column()) + { + case LineOrStationCol: + return item.lineOrStationId; + case KmCol: + return item.kmInMeters; + case DirectionCol: + return int(item.direction); + } + break; + } + case Qt::TextAlignmentRole: + { + if(idx.column() == KmCol) + return Qt::AlignRight + Qt::AlignVCenter; + break; + } + case DIRECTION_ROLE: + return int(item.direction); + } + + return QVariant(); +} + +bool RailwayNodeModel::setData(const QModelIndex &idx, const QVariant &value, int role) +{ + const int row = idx.row(); + if(!idx.isValid() || row >= curItemCount || idx.column() >= NCols + || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + Item &item = cache[row - cacheFirstRow]; + + switch (role) + { + case Qt::EditRole: + { + switch (idx.column()) + { + case KmCol: + { + bool ok = false; + int kmInMeters = value.toInt(&ok); + if(!setKm(item, kmInMeters)) + return false; + break; + } + case DirectionCol: + { + Direction dir = Direction(value.toBool()); + if(!setDirection(item, dir)) + return false; + break; + } + default: + return false; + } + break; + } + case DIRECTION_ROLE: + { + bool ok = false; + Direction dir = Direction(value.toInt(&ok)); + if(!ok || !setDirection(item, dir)) + return false; + break; + } + } + + emit dataChanged(idx, idx); + return true; +} + +Qt::ItemFlags RailwayNodeModel::flags(const QModelIndex &idx) const +{ + if (!idx.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren; + if(idx.row() < cacheFirstRow || idx.row() >= cacheFirstRow + cache.size()) + return f; //Not fetched yet + + f.setFlag(Qt::ItemIsEditable); + + return f; +} + +void RailwayNodeModel::clearCache() +{ + cache.clear(); + cache.squeeze(); + cacheFirstRow = 0; +} + +void RailwayNodeModel::refreshData() +{ + if(!mDb.db()) + return; + + query q(mDb, m_mode == RailwayNodeMode::StationLinesMode ? + "SELECT COUNT(1) FROM railways WHERE stationId=?" : + "SELECT COUNT(1) FROM railways WHERE lineId=?"); + q.bind(1, stationOrLineId); + 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 RailwayNodeModel::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; + // 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 RailwayNodeModel::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) TODO: enable + // { + // offset = 0; + // reverse = true; + // } + + //qDebug() << "Fetching:" << first << "ValRow:" << valRow << val << "Offset:" << offset << "Reverse:" << reverse; + + const char *orderCol; + + QByteArray sql; + if(m_mode == RailwayNodeMode::StationLinesMode) + { + sql = "SELECT r.id,r.lineId,r.pos_meters,r.direction,lines.name" + " FROM railways r" + " LEFT JOIN lines ON lines.id=r.lineId" + " WHERE r.stationId=?3"; + } + else + { + sql = "SELECT r.id,r.stationId,r.pos_meters,r.direction,stations.name" + " FROM railways r" + " LEFT JOIN stations ON stations.id=r.stationId" + " WHERE r.lineId=?3"; + } + + switch (sortCol) + { + case LineOrStationCol: + { + orderCol = m_mode == RailwayNodeMode::StationLinesMode ? + "lines.name" : + "stations.name"; + break; + } + case KmCol: + { + orderCol = "r.pos_meters"; + break; + } + case DirectionCol: + { + orderCol = m_mode == RailwayNodeMode::StationLinesMode ? + "r.direction,lines.name" : + "r.direction,stations.name"; + break; + } + } + + // if(val.isValid()) + // { + // sql += " WHERE "; + // sql += orderCol; + // if(reverse) + // sql += "?3"; + // } + + sql += " ORDER BY "; + sql += orderCol; + + 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, stationOrLineId); + + // if(val.isValid()) + // { + // switch (sortCol) + // { + // case Model: + // { + // 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; + Item &item = vec[i]; + item.nodeId = r.get(0); + item.lineOrStationId = r.get(1); + item.kmInMeters = r.get(2); + item.direction = Direction(r.get(3)); + item.lineOrStationName = r.get(4); + i--; + } + if(i > -1) + vec.remove(0, i + 1); + } + else + { + int i = 0; + + for(; it != end; ++it) + { + auto r = *it; + Item &item = vec[i]; + item.nodeId = r.get(0); + item.lineOrStationId = r.get(1); + item.kmInMeters = r.get(2); + item.direction = Direction(r.get(3)); + item.lineOrStationName = r.get(4); + i++; + } + if(i < BatchSize) + vec.remove(i, BatchSize - i); + } + + + RailwayNodeModelResultEvent *ev = new RailwayNodeModelResultEvent; + ev->items = vec; + ev->firstRow = first; + + qApp->postEvent(this, ev); +} + +void RailwayNodeModel::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); + + //qDebug() << "TOTAL: From:" << cacheFirstRow << "To:" << cacheFirstRow + cache.size() - 1; +} + +void RailwayNodeModel::setSortingColumn(int col) +{ + if(sortColumn == col || col >= NCols) + return; + + clearCache(); + sortColumn = col; + + QModelIndex first = index(0, 0); + QModelIndex last = index(curItemCount - 1, NCols - 1); + emit dataChanged(first, last); +} + +bool RailwayNodeModel::getFieldData(int row, int col, db_id &idOut, QString &nameOut) const +{ + if(row < cacheFirstRow || row >= cacheFirstRow + cache.size() || col != LineOrStationCol) + return false; + + const Item& item = cache[row - cacheFirstRow]; + idOut = item.lineOrStationId; + nameOut = item.lineOrStationName; + + return true; +} + +bool RailwayNodeModel::validateData(int /*row*/, int /*col*/, db_id /*id*/, const QString &/*name*/) +{ + return true; +} + +bool RailwayNodeModel::setFieldData(int row, int col, db_id id, const QString &name) +{ + if(row < cacheFirstRow || row >= cacheFirstRow + cache.size() || col != LineOrStationCol) + return false; + + Item& item = cache[row - cacheFirstRow]; + if(setLineOrStation(item, id, name)) + { + QModelIndex first = index(row, RailwayNodeModel::LineOrStationCol); + QModelIndex last = index(row, RailwayNodeModel::KmCol); + emit dataChanged(first, last); + return true; + } + return false; +} + +void RailwayNodeModel::setMode(db_id id, RailwayNodeMode mode) +{ + stationOrLineId = id; + m_mode = mode; + clearCache(); + refreshData(); +} + +db_id RailwayNodeModel::addItem(db_id lineOrStationId, int *outRow) +{ + db_id nodeId = 0; + + db_id lineId; + db_id stationId; + + if(m_mode == RailwayNodeMode::StationLinesMode) + { + lineId = lineOrStationId; + stationId = stationOrLineId; + }else{ + lineId = stationOrLineId; + stationId = lineOrStationId; + } + int kmInMeters = getLineMaxKmInMeters(lineId); + + command cmd(mDb, "INSERT INTO railways (id,lineId,stationId,pos_meters,direction)" + "VALUES(NULL,?,?,?,0)"); + cmd.bind(1, lineId); + cmd.bind(2, stationId); + cmd.bind(3, kmInMeters); + + sqlite3_mutex *mutex = sqlite3_db_mutex(mDb.db()); + sqlite3_mutex_enter(mutex); + int ret = cmd.execute(); + if(ret == SQLITE_OK) + { + nodeId = mDb.last_insert_rowid(); + } + sqlite3_mutex_leave(mutex); + + if(ret == SQLITE_CONSTRAINT_UNIQUE) + { + //There is already an entry with same line/station + //ERRORMSG + nodeId = 0; + } + else if(ret != SQLITE_OK) + { + qDebug() << "RailwayNodeModel Error adding:" << ret << mDb.error_msg() << mDb.error_code() << mDb.extended_error_code(); + } + + if(nodeId) + { + Session->mLineStorage->addStationToLine(lineId, stationId); + Session->mLineStorage->redrawLine(lineId); + refreshData(); //Recalc row count + switchToPage(0); //Reset to first page and so it is shown as first row + } + + if(outRow) + *outRow = nodeId ? 0 : -1; //Empty model is always the first + + return nodeId; +} + +bool RailwayNodeModel::removeItem(int row) +{ + if(row >= curItemCount || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + const Item &item = cache.at(row - cacheFirstRow); + command cmd(mDb, "DELETE FROM railways WHERE id=?"); + cmd.bind(1, item.nodeId); + int ret = cmd.execute(); + + db_id lineId; + db_id stationId; + + if(m_mode == RailwayNodeMode::StationLinesMode) + { + lineId = item.lineOrStationId; + stationId = stationOrLineId; + }else{ + lineId = stationOrLineId; + stationId = item.lineOrStationId; + } + + if(ret != SQLITE_OK) + { + qDebug() << Q_FUNC_INFO << "RailwayNodeModel Error:" << ret << mDb.error_msg() << "Node:" << item.nodeId; + int err = mDb.extended_error_code(); + if(err == SQLITE_CONSTRAINT_TRIGGER) + qDebug() << "Cannot remove station" << stationId << "IS IN USE"; + return false; + } + + if(lineId && stationId) + { + Session->mLineStorage->removeStationFromLine(lineId, stationId); + Session->mLineStorage->redrawLine(lineId); + } + + refreshData(); + return true; +} + +bool RailwayNodeModel::setLineOrStation(Item &item, db_id id, const QString &name) +{ + if(item.lineOrStationId == id) + return true; + + if(!id) + return false; + + command q_setLine(mDb, m_mode == RailwayNodeMode::StationLinesMode ? + "UPDATE railways SET lineId=? WHERE id=?" : + "UPDATE railways SET stationId=? WHERE id=?"); + q_setLine.bind(1, item.lineOrStationId); + q_setLine.bind(2, item.nodeId); + int ret = q_setLine.execute(); + + if(ret != SQLITE_OK) + { + int err = mDb.extended_error_code(); + if(err == SQLITE_CONSTRAINT_UNIQUE) + { + //The requested stations already is in this line (prevent duplicates) + //ERRORMSG: show messagebox + qDebug() << Q_FUNC_INFO << "Error: station - line couple IS NOT UNIQUE"; + } + return false; + } + + LineStorage *lineStorage = Session->mLineStorage; + db_id lineId = 0; + if(m_mode == RailwayNodeMode::LineStationsMode) + { + //Remove previous station if any + lineId = stationOrLineId; + if(item.lineOrStationId != 0) + lineStorage->removeStationFromLine(lineId, item.lineOrStationId); + + item.lineOrStationId = id; //Set new station + if(item.lineOrStationId != 0) + lineStorage->addStationToLine(lineId, item.lineOrStationId); + } + else + { + lineId = id; + int kmInMeters = getLineMaxKmInMeters(lineId); + setNodeKm(item.nodeId, kmInMeters); + + //Remove from previous line if any + if(item.lineOrStationId != 0) + { + lineStorage->removeStationFromLine(item.lineOrStationId, stationOrLineId); + lineStorage->redrawLine(item.lineOrStationId); //Redraw old line + } + + item.kmInMeters = kmInMeters; + item.lineOrStationId = lineId; + if(item.lineOrStationId != 0) + lineStorage->addStationToLine(item.lineOrStationId, stationOrLineId); + } + + item.lineOrStationName = name; + if(lineId) + lineStorage->redrawLine(lineId); //Redraw new line + + //This migth change also km, and when sorting by direction the name is also taken in account + //so reload data in every case + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + + return true; +} + +bool RailwayNodeModel::setKm(Item &item, int kmInMeters) +{ + if(item.kmInMeters == kmInMeters) + return true; + + db_id lineId = m_mode == RailwayNodeMode::StationLinesMode ? item.lineOrStationId : stationOrLineId; + if(!lineId) + return false; //Cannot set 'km' before setting 'line' + + //Invalid (km < 0) or there's another station already at that km + if(kmInMeters < 0) + return false; //ERRORMSG: show message box + + query q_lineHasStAtKm(mDb, "SELECT stationId FROM railways WHERE lineId=? AND pos_meters=?"); + q_lineHasStAtKm.bind(1, lineId); + q_lineHasStAtKm.bind(2, kmInMeters); + int ret = q_lineHasStAtKm.step(); + + //There's already a station at that km + if(ret == SQLITE_ROW) + return false; + + if(!setNodeKm(item.nodeId, kmInMeters)) + return false; + + item.kmInMeters = kmInMeters; + + //Update graph and jobs + Session->mLineStorage->redrawLine(lineId); + + if(sortColumn == KmCol) + { + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + } + + return true; +} + +bool RailwayNodeModel::setDirection(Item &item, Direction dir) +{ + if(item.direction == dir) + return true; + + command q_setDirection (mDb, "UPDATE railways SET direction=? WHERE id=?"); + q_setDirection.bind(1, int(dir)); + q_setDirection.bind(2, item.nodeId); + int ret = q_setDirection.execute(); + if(ret != SQLITE_OK) + return false; + + item.direction = dir; + + if(sortColumn == DirectionCol) + { + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + } + + return true; +} + +int RailwayNodeModel::getLineMaxKmInMeters(db_id lineId) +{ + query q_getLineMaxKm (mDb, "SELECT MAX(pos_meters) FROM railways WHERE lineId=?"); + q_getLineMaxKm.bind(1, lineId); + q_getLineMaxKm.step(); + auto row = q_getLineMaxKm.getRows(); + + int kmInMeters = 0; + //If MAX(km) returns NULL leave 'km' = 0 + //Otherwise add +1 to last station km + if(row.column_type(0) != SQLITE_NULL) + { + kmInMeters = row.get(0) + 1000; //Add 1 km (1000 meters) to highest km in the line + } + return kmInMeters; +} + +bool RailwayNodeModel::setNodeKm(db_id nodeId, int kmInMeters) +{ + command q_setKm(mDb, "UPDATE railways SET pos_meters=? WHERE id=?"); + q_setKm.bind(1, kmInMeters); + q_setKm.bind(2, nodeId); + int ret = q_setKm.execute(); + + if(ret != SQLITE_OK) + { + qWarning() << "RailwayNodeModel Error: setting km for node:" << nodeId << mDb.error_msg() << ret; + return false; + } + return true; +} diff --git a/src/stations/manager/railwaynode/railwaynodemodel.h b/src/stations/manager/railwaynode/railwaynodemodel.h new file mode 100644 index 0000000..33dd1a8 --- /dev/null +++ b/src/stations/manager/railwaynode/railwaynodemodel.h @@ -0,0 +1,99 @@ +#ifndef RAILWAYNODEMODEL_H +#define RAILWAYNODEMODEL_H + +#include "utils/sqldelegate/pageditemmodel.h" +#include "utils/sqldelegate/IFKField.h" + +#include "utils/types.h" +#include "utils/directiontype.h" +#include "railwaynodemode.h" + +class RailwayNodeModel : public IPagedItemModel, public IFKField +{ + Q_OBJECT +public: + + enum { BatchSize = 100 }; + + typedef enum { + LineOrStationCol = 0, + KmCol, + DirectionCol, + NCols + } Columns; + + class Item + { + public: + db_id nodeId; + db_id lineOrStationId; + QString lineOrStationName; + int kmInMeters; + Direction direction; + }; + + RailwayNodeModel(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; + + // Editable: + bool setData(const QModelIndex &idx, const QVariant &value, + int role = Qt::EditRole) override; + + Qt::ItemFlags flags(const QModelIndex& idx) 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; + + // IFKField + bool getFieldData(int row, int col, db_id &idOut, QString &nameOut) const override; + bool validateData(int row, int col, db_id id, const QString &name) override; + bool setFieldData(int row, int col, db_id id, const QString &name) override; + + // RailwayNodeModel + inline RailwayNodeMode getMode() const { return m_mode; } + + void setMode(db_id id, RailwayNodeMode mode); + db_id addItem(db_id lineOrStationId, int *outRow); + bool removeItem(int row); + +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); + + bool setLineOrStation(Item &item, db_id id, const QString &name); + bool setKm(Item &item, int kmInMeters); + bool setDirection(Item &item, Direction dir); + + int getLineMaxKmInMeters(db_id lineId); + bool setNodeKm(db_id nodeId, int kmInMeters); + +private: + QVector cache; + int cacheFirstRow; + int firstPendingRow; + + db_id stationOrLineId; + RailwayNodeMode m_mode; +}; + +#endif // RAILWAYNODEMODEL_H diff --git a/src/stations/manager/railwaynode/stationorlinematchfactory.cpp b/src/stations/manager/railwaynode/stationorlinematchfactory.cpp new file mode 100644 index 0000000..1eec6c9 --- /dev/null +++ b/src/stations/manager/railwaynode/stationorlinematchfactory.cpp @@ -0,0 +1,30 @@ +#include "stationorlinematchfactory.h" + +#include "stations/stationsmatchmodel.h" +#include "lines/linesmatchmodel.h" + +StationOrLineMatchFactory::StationOrLineMatchFactory(sqlite3pp::database &db, QObject *parent) : + IMatchModelFactory(parent), + mDb(db), + m_mode(RailwayNodeMode::StationLinesMode) +{ + +} + +ISqlFKMatchModel *StationOrLineMatchFactory::createModel() +{ + switch (m_mode) + { + case RailwayNodeMode::StationLinesMode: + { + return new LinesMatchModel(mDb, false); + } + case RailwayNodeMode::LineStationsMode: + { + StationsMatchModel *m = new StationsMatchModel(mDb); + m->setFilter(0, 0); + return m; + } + } + return nullptr; +} diff --git a/src/stations/manager/railwaynode/stationorlinematchfactory.h b/src/stations/manager/railwaynode/stationorlinematchfactory.h new file mode 100644 index 0000000..c1ba213 --- /dev/null +++ b/src/stations/manager/railwaynode/stationorlinematchfactory.h @@ -0,0 +1,26 @@ +#ifndef STATIONORLINEMATCHFACTORY_H +#define STATIONORLINEMATCHFACTORY_H + +#include "utils/sqldelegate/imatchmodelfactory.h" +#include "railwaynodemode.h" + +namespace sqlite3pp +{ +class database; +} + +class StationOrLineMatchFactory : public IMatchModelFactory +{ +public: + StationOrLineMatchFactory(sqlite3pp::database &db, QObject *parent); + + ISqlFKMatchModel *createModel() override; + + inline void setMode(RailwayNodeMode mode) { m_mode = mode; } + +private: + sqlite3pp::database &mDb; + RailwayNodeMode m_mode; +}; + +#endif // STATIONORLINEMATCHFACTORY_H diff --git a/src/stations/manager/stationjobview.cpp b/src/stations/manager/stationjobview.cpp new file mode 100644 index 0000000..0bbe5c4 --- /dev/null +++ b/src/stations/manager/stationjobview.cpp @@ -0,0 +1,136 @@ +#include "stationjobview.h" +#include "ui_stationjobview.h" + +#include "utils/platform_utils.h" + +#include "app/session.h" + +#include "viewmanager/viewmanager.h" + +#include "app/scopedebug.h" + +#include +#include + +#include + +#include +#include + +#include "odt_export/stationsheetexport.h" + +#include "stationplanmodel.h" + +#include "utils/file_format_names.h" + +StationJobView::StationJobView(QWidget *parent) : + QWidget(parent), + ui(new Ui::StationJobView), + m_stationId(0) +{ + ui->setupUi(this); + + model = new StationPlanModel(Session->m_Db, this); + + ui->tableView->setModel(model); + + ui->tableView->setColumnWidth(0, 60); //Arrival + ui->tableView->setColumnWidth(1, 60); //Departure + ui->tableView->setColumnWidth(2, 55); //Platform + ui->tableView->setColumnWidth(3, 90); //Job + ui->tableView->setColumnWidth(4, 115); //Notes + ui->tableView->setSelectionBehavior(QTableView::SelectRows); + + ui->tableView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->tableView, &QTableView::customContextMenuRequested, this, &StationJobView::showContextMenu); + + connect(ui->sheetButton, &QPushButton::pressed, this, &StationJobView::onSaveSheet); + connect(ui->updateButt, &QPushButton::clicked, this, &StationJobView::updateInfo); + + setMinimumSize(300, 200); + resize(450, 300); +} + +StationJobView::~StationJobView() +{ + delete ui; +} + +void StationJobView::setStation(db_id stId) +{ + m_stationId = stId; +} + +void StationJobView::updateName() +{ + query q(Session->m_Db, "SELECT name FROM stations WHERE id=?"); + q.bind(1, m_stationId); + q.step(); + setWindowTitle(q.getRows().get(0)); +} + +void StationJobView::updateInfo() +{ + updateName(); + updateJobsList(); +} + +void StationJobView::updateJobsList() +{ + model->loadPlan(m_stationId); +} + +void StationJobView::showContextMenu(const QPoint& pos) +{ + DEBUG_ENTRY; + + QModelIndex idx = ui->tableView->indexAt(pos); + if(!idx.isValid()) + return; + + std::pair item = model->getJobAndStopId(idx.row()); + + QMenu menu(this); + + QAction *showInJobEditor = new QAction(tr("Show in Job Editor"), &menu); + QAction *selectJobInGraph = new QAction(tr("Show job in graph"), &menu); + menu.addAction(showInJobEditor); + menu.addAction(selectJobInGraph); + + QAction *act = menu.exec(ui->tableView->viewport()->mapToGlobal(pos)); + if(act == showInJobEditor) + { + Session->getViewManager()->requestJobEditor(item.first, item.second); + } + else if(act == selectJobInGraph) + { + Session->getViewManager()->requestJobSelection(item.first, true, true); + } +} + +void StationJobView::onSaveSheet() +{ + DEBUG_ENTRY; + + QFileDialog dlg(this, tr("Save Station Sheet")); + dlg.setFileMode(QFileDialog::AnyFile); + dlg.setAcceptMode(QFileDialog::AcceptSave); + dlg.setDirectory(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); + dlg.selectFile(tr("%1_station.odt").arg(windowTitle())); + + 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; + + StationSheetExport sheet(m_stationId); + sheet.write(); + sheet.save(fileName); +} diff --git a/src/stations/manager/stationjobview.h b/src/stations/manager/stationjobview.h new file mode 100644 index 0000000..675c572 --- /dev/null +++ b/src/stations/manager/stationjobview.h @@ -0,0 +1,44 @@ +#ifndef STATIONJOBVIEW_H +#define STATIONJOBVIEW_H + +#include + +#include "utils/types.h" + +namespace Ui { +class StationJobView; +} + +class StationPlanModel; + +typedef struct Stop_ Stop; + +class StationJobView : public QWidget +{ + Q_OBJECT + +public: + explicit StationJobView(QWidget *parent = nullptr); + ~StationJobView(); + + void setStation(db_id stId); + + void updateJobsList(); + void updateName(); + +private slots: + void showContextMenu(const QPoint &pos); + void onSaveSheet(); + void updateInfo(); + +private: + Ui::StationJobView *ui; + + StationPlanModel *model; + + db_id m_stationId; + + QPixmap mGraph; +}; + +#endif // STATIONJOBVIEW_H diff --git a/src/stations/manager/stationjobview.ui b/src/stations/manager/stationjobview.ui new file mode 100644 index 0000000..80ffeda --- /dev/null +++ b/src/stations/manager/stationjobview.ui @@ -0,0 +1,42 @@ + + + StationJobView + + + + 0 + 0 + 400 + 300 + + + + + + + + + + Update + + + + + + + Save sheet + + + + + + + QAbstractItemView::NoEditTriggers + + + + + + + + diff --git a/src/stations/manager/stationplanmodel.cpp b/src/stations/manager/stationplanmodel.cpp new file mode 100644 index 0000000..39effd7 --- /dev/null +++ b/src/stations/manager/stationplanmodel.cpp @@ -0,0 +1,195 @@ +#include "stationplanmodel.h" + +#include "utils/platform_utils.h" +#include "utils/jobcategorystrings.h" + +#include + +StationPlanModel::StationPlanModel(sqlite3pp::database &db, QObject *parent) : + QAbstractTableModel(parent), + mDb(db), + q_countPlanItems(mDb, "SELECT COUNT(id) FROM stops WHERE stationId=?"), + q_selectPlan(mDb, "SELECT stops.id," + "jobs.category," + "jobs.id," + "stops.arrival," + "stops.departure," + "stops.platform," + "stops.transit" + " FROM stops" + " JOIN jobs ON jobs.id=stops.jobId" + " WHERE stops.stationId=?" + " ORDER BY stops.arrival,stops.platform") +{ +} + +QVariant StationPlanModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch (section) + { + case Arrival: + return tr("Arrival"); + case Departure: + return tr("Departure"); + case Platform: + return tr("Platform"); + case Job: + return tr("Job"); + case Notes: + return tr("Notes"); + default: + break; + } + } + return QAbstractTableModel::headerData(section, orientation, role); +} + +int StationPlanModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_data.size(); +} + +int StationPlanModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant StationPlanModel::data(const QModelIndex &idx, int role) const +{ + if (!idx.isValid() || idx.row() >= m_data.size() || idx.column() >= NCols) + return QVariant(); + + const StPlanItem& item = m_data.at(idx.row()); + + switch (role) + { + case Qt::DisplayRole: + { + + switch (idx.column()) + { + case Arrival: + { + if(item.type == StPlanItem::ItemType::Departure) + break; //Don't repeat arrival also in the second row. + return item.arrival; + } + case Departure: + return item.departure; + case Platform: + return utils::platformName(item.platform); + case Job: + return JobCategoryName::jobName(item.jobId, item.cat); + case Notes: + { + if(item.type == StPlanItem::ItemType::Departure) + return StationPlanModel::tr("Departure"); //Don't repeat description also in the second row. + + return item.description; + } + } + break; + } + case Qt::ForegroundRole: + { + if(item.type == StPlanItem::ItemType::Transit) + return QColor(0, 0, 255); //Transit in blue + break; + } + case Qt::ToolTipRole: + { + if(item.type == StPlanItem::ItemType::Transit) + return StationPlanModel::tr("Transit"); + break; + } + } + + return QVariant(); +} + +void StationPlanModel::clear() +{ + beginResetModel(); + m_data.clear(); + m_data.squeeze(); + endResetModel(); +} + +void StationPlanModel::loadPlan(db_id stId) +{ + beginResetModel(); + + m_data.clear(); + + q_countPlanItems.bind(1, stId); + q_countPlanItems.step(); + int count = q_countPlanItems.getRows().get(0); + q_countPlanItems.reset(); + + if(count > 0) + { + QMap stopMap; //Order by Departure ASC + + q_selectPlan.bind(1, stId); + for(auto r : q_selectPlan) + { + StPlanItem curStop; + curStop.stopId = r.get(0); + curStop.cat = JobCategory(r.get(1)); + curStop.jobId = r.get(2); + curStop.arrival = r.get(3); + curStop.departure = r.get(4); + curStop.platform = r.get(5); + curStop.type = StPlanItem::ItemType::Normal; + int stopType = r.get(6); + + for(auto stop = stopMap.begin(); stop != stopMap.end(); /*nothing because of erase */) + { + if(stop->departure <= curStop.arrival) + { + m_data.append(stop.value()); + m_data.last().type = StPlanItem::ItemType::Departure; + stop = stopMap.erase(stop); + } + else + { + ++stop; + } + } + + m_data.append(curStop); + if(stopType == Transit) + { + m_data.last().type = StPlanItem::ItemType::Transit; + } + else + { + //Transit should not be repeated + curStop.type = StPlanItem::ItemType::Departure; + stopMap.insert(curStop.departure, std::move(curStop)); + } + } + q_selectPlan.reset(); + + for(const StPlanItem& stop : stopMap) + { + m_data.append(stop); + } + } + + m_data.squeeze(); + + endResetModel(); +} + +std::pair StationPlanModel::getJobAndStopId(int row) const +{ + if(row < m_data.size()) + { + const StPlanItem& item = m_data.at(row); + return {item.jobId, item.stopId}; + } + return {0, 0}; +} diff --git a/src/stations/manager/stationplanmodel.h b/src/stations/manager/stationplanmodel.h new file mode 100644 index 0000000..1c3ca98 --- /dev/null +++ b/src/stations/manager/stationplanmodel.h @@ -0,0 +1,72 @@ +#ifndef STATIONPLANMODEL_H +#define STATIONPLANMODEL_H + +#include + +#include + +#include + +#include "utils/types.h" + +typedef struct StPlanItem_ +{ + db_id stopId; + db_id jobId; + QTime arrival; + QTime departure; + int platform; + + QString description; + + JobCategory cat; + + enum class ItemType : qint8 + { + Normal = 0, + Departure, + Transit + }; + ItemType type; +} StPlanItem; + +class StationPlanModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + enum Columns + { + Arrival = 0, + Departure, + Platform, + Job, + Notes, + NCols + }; + StationPlanModel(sqlite3pp::database& db, QObject *parent = nullptr); + + // 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; + + void clear(); + + void loadPlan(db_id stId); + + std::pair getJobAndStopId(int row) const; + +private: + QVector m_data; + + sqlite3pp::database& mDb; + sqlite3pp::query q_countPlanItems; + sqlite3pp::query q_selectPlan; +}; + +#endif // STATIONPLANMODEL_H diff --git a/src/stations/manager/stationsmanager.cpp b/src/stations/manager/stationsmanager.cpp new file mode 100644 index 0000000..7a4dbe8 --- /dev/null +++ b/src/stations/manager/stationsmanager.cpp @@ -0,0 +1,402 @@ +#include "stationsmanager.h" +#include "ui_stationsmanager.h" + +#include "app/session.h" +#include "viewmanager/viewmanager.h" + +#include "app/scopedebug.h" + +#include +#include +#include + +#include + +#include + +#include "stations/stationssqlmodel.h" +#include "lines/linessqlmodel.h" + +#include "colordelegate.h" +#include "defaultplatfdelegate.h" +#include "utils/spinbox/spinboxeditorfactory.h" +#include "utils/sqldelegate/modelpageswitcher.h" + +#include "stations/manager/free_rs_viewer/stationfreersviewer.h" //TODO: move to ViewManager + +#include "railwaynode/railwaynodeeditor.h" + +StationsManager::StationsManager(QWidget *parent) : + QWidget(parent), + ui(new Ui::StationsManager), + oldCurrentTab(StationsTab), + clearModelTimers{0, 0}, + m_readOnly(false), + windowConnected(false) +{ + ui->setupUi(this); + setup_StPage(); + setup_LinePage(); + + connect(stationsModel, &StationsSQLModel::modelError, this, &StationsManager::onModelError); + connect(linesModel, &LinesSQLModel::modelError, this, &StationsManager::onModelError); + + setReadOnly(false); + + ui->tabWidget->setCurrentIndex(StationsTab); + connect(ui->tabWidget, &QTabWidget::currentChanged, this, &StationsManager::updateModels); + + setWindowTitle(tr("Stations Manager")); +} + +StationsManager::~StationsManager() +{ + delete ui; + delete stationPlatfCountFactory; + delete lineSpeedSpinFactory; + + for(int i = 0; i < NTabs; i++) + { + if(clearModelTimers[i] > 0) + { + killTimer(clearModelTimers[i]); + clearModelTimers[i] = 0; + } + } +} + +void StationsManager::setup_StPage() +{ + QVBoxLayout *vboxLayout = new QVBoxLayout(ui->stationsTab); + stationToolBar = new QToolBar(ui->stationsTab); + vboxLayout->addWidget(stationToolBar); + + stationView = new QTableView(ui->stationsTab); + vboxLayout->addWidget(stationView); + + stationsModel = new StationsSQLModel(Session->m_Db, this); + stationView->setModel(stationsModel); + + auto ps = new ModelPageSwitcher(false, this); + vboxLayout->addWidget(ps); + ps->setModel(stationsModel); + //Custom colun sorting + //NOTE: leave disconnect() in the old SIGLAL()/SLOT() version in order to work + QHeaderView *header = stationView->horizontalHeader(); + disconnect(header, SIGNAL(sectionPressed(int)), stationView, SLOT(selectColumn(int))); + disconnect(header, SIGNAL(sectionEntered(int)), stationView, SLOT(_q_selectColumn(int))); + connect(header, &QHeaderView::sectionClicked, this, [this, header](int section) + { + stationsModel->setSortingColumn(section); + header->setSortIndicator(stationsModel->getSortingColumn(), Qt::AscendingOrder); + }); + header->setSortIndicatorShown(true); + header->setSortIndicator(stationsModel->getSortingColumn(), Qt::AscendingOrder); + + QStyledItemDelegate *platfCountDelegate = new QStyledItemDelegate(this); + stationPlatfCountFactory = new SpinBoxEditorFactory; + stationPlatfCountFactory->setRange(0, 99); + platfCountDelegate->setItemEditorFactory(stationPlatfCountFactory); + stationView->setItemDelegateForColumn(StationsSQLModel::Platforms, platfCountDelegate); + stationView->setItemDelegateForColumn(StationsSQLModel::Depots, platfCountDelegate); + + stationView->setItemDelegateForColumn(StationsSQLModel::PlatformColor, new ColorDelegate(this)); + + DefaultPlatfDelegate *defPlatfDelegate = new DefaultPlatfDelegate(this); + stationView->setItemDelegateForColumn(StationsSQLModel::DefaultFreightPlatf, defPlatfDelegate); + stationView->setItemDelegateForColumn(StationsSQLModel::DefaultPassengerPlatf, defPlatfDelegate); + + + act_addSt = stationToolBar->addAction(tr("Add"), this, &StationsManager::onNewStation); + act_remSt = stationToolBar->addAction(tr("Remove"), this, &StationsManager::onRemoveStation); + act_planSt = stationToolBar->addAction(tr("Plan"), this, &StationsManager::showStPlan); + act_freeRs = stationToolBar->addAction(tr("Free RS"), this, &StationsManager::onShowFreeRS); + act_editSt = stationToolBar->addAction(tr("Edit"), this, &StationsManager::onEditStation); +} + +void StationsManager::setup_LinePage() +{ + QVBoxLayout *vboxLayout = new QVBoxLayout(ui->linesTab); + linesToolBar = new QToolBar(ui->linesTab); + vboxLayout->addWidget(linesToolBar); + + linesView = new QTableView(ui->linesTab); + vboxLayout->addWidget(linesView); + + linesModel = new LinesSQLModel(Session->m_Db, this); + linesView->setModel(linesModel); + + auto ps = new ModelPageSwitcher(false, this); + vboxLayout->addWidget(ps); + ps->setModel(linesModel); + //Custom colun sorting + //NOTE: leave disconnect() in the old SIGLAL()/SLOT() version in order to work + QHeaderView *header = linesView->horizontalHeader(); + disconnect(header, SIGNAL(sectionPressed(int)), linesView, SLOT(selectColumn(int))); + disconnect(header, SIGNAL(sectionEntered(int)), linesView, SLOT(_q_selectColumn(int))); + connect(header, &QHeaderView::sectionClicked, this, [this, header](int section) + { + linesModel->setSortingColumn(section); + header->setSortIndicator(linesModel->getSortingColumn(), Qt::AscendingOrder); + }); + header->setSortIndicatorShown(true); + header->setSortIndicator(linesModel->getSortingColumn(), Qt::AscendingOrder); + + QStyledItemDelegate *lineSpeedDelegate = new QStyledItemDelegate(this); + lineSpeedSpinFactory = new SpinBoxEditorFactory; + lineSpeedSpinFactory->setRange(1, 999); + lineSpeedSpinFactory->setSuffix(" km/h"); + lineSpeedSpinFactory->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + lineSpeedDelegate->setItemEditorFactory(lineSpeedSpinFactory); + linesView->setItemDelegateForColumn(LinesSQLModel::LineMaxSpeedKmHCol, lineSpeedDelegate); + + linesToolBar->addAction(tr("Add"), this, &StationsManager::onNewLine); + linesToolBar->addAction(tr("Remove"), this, &StationsManager::onRemoveLine); + linesToolBar->addAction(tr("Edit"), this, &StationsManager::onEditLine); +} + +void StationsManager::showEvent(QShowEvent *e) +{ + if(!windowConnected) + { + QWindow *w = windowHandle(); + if(w) + { + windowConnected = true; + connect(w, &QWindow::visibilityChanged, this, &StationsManager::visibilityChanged); + updateModels(); + } + } + QWidget::showEvent(e); +} + +void StationsManager::timerEvent(QTimerEvent *e) +{ + if(e->timerId() == clearModelTimers[StationsTab]) + { + stationsModel->clearCache(); + killTimer(e->timerId()); + clearModelTimers[StationsTab] = ModelCleared; + return; + } + else if(e->timerId() == clearModelTimers[LinesTab]) + { + linesModel->clearCache(); + killTimer(e->timerId()); + clearModelTimers[LinesTab] = ModelCleared; + return; + } + + QWidget::timerEvent(e); +} + +void StationsManager::visibilityChanged(int v) +{ + if(v == QWindow::Minimized || v == QWindow::Hidden) + { + //If the window is minimized start timer to clear model cache of current tab + //The other tabs already have been cleared or are waiting with their timers + if(clearModelTimers[oldCurrentTab] == ModelLoaded) + { + clearModelTimers[oldCurrentTab] = startTimer(ClearModelTimeout, Qt::VeryCoarseTimer); + } + }else{ + updateModels(); + } +} + +void StationsManager::updateModels() +{ + int curTab = ui->tabWidget->currentIndex(); + + if(clearModelTimers[curTab] > 0) + { + //This page was already cached, stop it from clearing + killTimer(clearModelTimers[curTab]); + } + else if(clearModelTimers[curTab] == ModelCleared) + { + //This page wasn't already cached + switch (curTab) + { + case StationsTab: + { + stationsModel->refreshData(); + break; + } + case LinesTab: + { + linesModel->refreshData(); + break; + } + } + } + clearModelTimers[curTab] = ModelLoaded; + + //Now start timer to clear old current page if not already done + if(oldCurrentTab != curTab && clearModelTimers[oldCurrentTab] == ModelLoaded) //Wait 10 seconds and then clear cache + { + clearModelTimers[oldCurrentTab] = startTimer(ClearModelTimeout, Qt::VeryCoarseTimer); + } + + oldCurrentTab = curTab; +} + +void StationsManager::onRemoveStation() +{ + DEBUG_ENTRY; + + if(!stationView->selectionModel()->hasSelection()) + return; + + db_id stId = stationsModel->getIdAtRow(stationView->currentIndex().row()); + if(!stId) + return; + + stationsModel->removeStation(stId); +} + +void StationsManager::onNewStation() +{ + DEBUG_ENTRY; + + int row = 0; + if(!stationsModel->addStation()) + { + QMessageBox::warning(this, + tr("Error Adding Station"), + tr("An error occurred while adding a new station:\n%1") + .arg(Session->m_Db.error_msg())); + return; + } + + QModelIndex idx = stationsModel->index(row, 0); + stationView->setCurrentIndex(idx); + stationView->scrollTo(idx); + stationView->edit(idx); +} + +void StationsManager::onModelError(const QString& msg) +{ + QMessageBox::warning(this, tr("Station error"), msg); +} + +void StationsManager::onEditStation() +{ + DEBUG_ENTRY; + if(!stationView->selectionModel()->hasSelection()) + return; + + QModelIndex idx = stationView->currentIndex(); + db_id stId = stationsModel->getIdAtRow(idx.row()); + if(!stId) + return; + QString stName = stationsModel->getNameAtRow(idx.row()); + + RailwayNodeEditor ed(Session->m_Db, this); + ed.setMode(stName, stId, RailwayNodeMode::StationLinesMode); + ed.exec(); +} + +void StationsManager::showStPlan() +{ + DEBUG_ENTRY; + if(!stationView->selectionModel()->hasSelection()) + return; + + QModelIndex idx = stationView->currentIndex(); + db_id stId = stationsModel->getIdAtRow(idx.row()); + if(!stId) + return; + Session->getViewManager()->requestStPlan(stId); +} + +void StationsManager::onShowFreeRS() +{ + DEBUG_COLOR_ENTRY(SHELL_BLUE); + if(!stationView->selectionModel()->hasSelection()) + return; + + QModelIndex idx = stationView->currentIndex(); + db_id stId = stationsModel->getIdAtRow(idx.row()); + if(!stId) + return; + Session->getViewManager()->requestStFreeRSViewer(stId); +} + +void StationsManager::onNewLine() +{ + DEBUG_ENTRY; + + int row = 0; + + if(!linesModel->addLine(&row) || row == -1) + { + QMessageBox::warning(this, + tr("Error Adding Line"), + tr("An error occurred while adding a new line:\n%1") + .arg(Session->m_Db.error_msg())); + return; + } + + QModelIndex idx = linesModel->index(row, 0); + linesView->setCurrentIndex(idx); + linesView->scrollTo(idx); + linesView->edit(idx); +} + +void StationsManager::onRemoveLine() +{ + DEBUG_ENTRY; + if(!linesView->selectionModel()->hasSelection()) + return; + + int row = linesView->currentIndex().row(); + db_id lineId = linesModel->getIdAtRow(row); + if(!lineId) + return; + linesModel->removeLine(lineId); +} + +void StationsManager::onEditLine() +{ + DEBUG_ENTRY; + if(!linesView->selectionModel()->hasSelection()) + return; + + int row = linesView->currentIndex().row(); + db_id lineId = linesModel->getIdAtRow(row); + if(!lineId) + return; + + const QString lineName = linesModel->getNameAtRow(row); + + RailwayNodeEditor ed(Session->m_Db, this); + ed.setMode(lineName, lineId, RailwayNodeMode::LineStationsMode); + ed.exec(); +} + +void StationsManager::setReadOnly(bool readOnly) +{ + if(m_readOnly == readOnly) + return; + + m_readOnly = readOnly; + + linesToolBar->setDisabled(m_readOnly); + + act_addSt->setDisabled(m_readOnly); + act_remSt->setDisabled(m_readOnly); + act_editSt->setDisabled(m_readOnly); + + if(m_readOnly) + { + stationView->setEditTriggers(QTableView::NoEditTriggers); + linesView->setEditTriggers(QTableView::NoEditTriggers); + } + else + { + stationView->setEditTriggers(QTableView::DoubleClicked); + linesView->setEditTriggers(QTableView::DoubleClicked); + } +} diff --git a/src/stations/manager/stationsmanager.h b/src/stations/manager/stationsmanager.h new file mode 100644 index 0000000..2cafedc --- /dev/null +++ b/src/stations/manager/stationsmanager.h @@ -0,0 +1,103 @@ +#ifndef STATIONSMANAGER_H +#define STATIONSMANAGER_H + +#include +#include + +#include "utils/types.h" + + +class QToolBar; +class QToolButton; +class QTableView; + +class StationsSQLModel; +class LinesSQLModel; + +namespace Ui { +class StationsManager; +} + +class SpinBoxEditorFactory; + +class StationsManager : public QWidget +{ + Q_OBJECT + +public: + + typedef enum + { + StationsTab = 0, + LinesTab, + NTabs + } Tabs; + + typedef enum + { + ModelCleared = 0, + ModelLoaded = -1 + } ModelState; + + enum { ClearModelTimeout = 5000 }; // 5 seconds + + explicit StationsManager(QWidget *parent = nullptr); + ~StationsManager() override; + + void fillLinesCombo(); + + void setReadOnly(bool readOnly = true); + +private slots: + void updateModels(); + void visibilityChanged(int v); + + void onModelError(const QString &msg); + + void onRemoveStation(); + void onNewStation(); + void onEditStation(); + + void onNewLine(); + void onRemoveLine(); + void onEditLine(); + void showStPlan(); + void onShowFreeRS(); + +private: + void setup_StPage(); + void setup_LinePage(); + +protected: + virtual void showEvent(QShowEvent *e) override; + virtual void timerEvent(QTimerEvent *e) override; + +private: + Ui::StationsManager *ui; + + QToolBar *stationToolBar; + QToolBar *linesToolBar; + + QTableView *stationView; + QTableView *linesView; + + StationsSQLModel *stationsModel; + LinesSQLModel *linesModel; + + SpinBoxEditorFactory *stationPlatfCountFactory; + SpinBoxEditorFactory *lineSpeedSpinFactory; + + QAction *act_addSt; + QAction *act_remSt; + QAction *act_editSt; + QAction *act_planSt; + QAction *act_freeRs; + + int oldCurrentTab; + int clearModelTimers[NTabs]; + + bool m_readOnly; + bool windowConnected; +}; + +#endif // STATIONSMANAGER_H diff --git a/src/stations/manager/stationsmanager.ui b/src/stations/manager/stationsmanager.ui new file mode 100644 index 0000000..41ec1c3 --- /dev/null +++ b/src/stations/manager/stationsmanager.ui @@ -0,0 +1,53 @@ + + + StationsManager + + + + 0 + 0 + 600 + 400 + + + + + + + + 3 + + + 3 + + + 3 + + + 3 + + + 1 + + + + + 0 + + + + Stations + + + + + Lines + + + + + + + + + diff --git a/src/stations/stationsmatchmodel.cpp b/src/stations/stationsmatchmodel.cpp new file mode 100644 index 0000000..337b3a5 --- /dev/null +++ b/src/stations/stationsmatchmodel.cpp @@ -0,0 +1,151 @@ +#include "stationsmatchmodel.h" + +StationsMatchModel::StationsMatchModel(database &db, QObject *parent) : + ISqlFKMatchModel(parent), + mDb(db), + q_getMatches(mDb), + m_lineId(0), + m_exceptStId(0) +{ + +} + +QVariant StationsMatchModel::data(const QModelIndex &idx, int role) const +{ + if (!idx.isValid() || idx.row() >= size) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + { + if(isEmptyRow(idx.row())) + { + return ISqlFKMatchModel::tr("Empty"); + } + else if(isEllipsesRow(idx.row())) + { + return ellipsesString; + } + + return items[idx.row()].name; + } + case Qt::FontRole: + { + if(isEmptyRow(idx.row())) + { + return boldFont(); + } + break; + } + } + + return QVariant(); +} + +void StationsMatchModel::autoSuggest(const QString &text) +{ + mQuery.clear(); + if(!text.isEmpty()) + { + mQuery.clear(); + mQuery.reserve(text.size() + 2); + mQuery.append('%'); + mQuery.append(text.toUtf8()); + mQuery.append('%'); + } + + refreshData(); +} + +void StationsMatchModel::refreshData() +{ + if(!mDb.db()) + return; + + beginResetModel(); + + char emptyQuery = '%'; + + if(mQuery.isEmpty()) + sqlite3_bind_text(q_getMatches.stmt(), 1, &emptyQuery, 1, SQLITE_STATIC); + else + sqlite3_bind_text(q_getMatches.stmt(), 1, mQuery, mQuery.size(), SQLITE_STATIC); + + if(m_exceptStId) + q_getMatches.bind(2, m_exceptStId); + if(m_lineId) + q_getMatches.bind(3, m_lineId); + + auto end = q_getMatches.end(); + auto it = q_getMatches.begin(); + int i = 0; + for(; i < MaxMatchItems && it != end; i++) + { + items[i].stationId = (*it).get(0); + items[i].name = (*it).get(1); + ++it; + } + + size = i + 1; //Items + Empty + + if(it != end) + { + //There would be still rows, show Ellipses + size++; //Items + Empty + Ellispses + } + + q_getMatches.reset(); + endResetModel(); + + emit resultsReady(false); +} + +QString StationsMatchModel::getName(db_id id) const +{ + if(!mDb.db()) + return QString(); + + query q(mDb, "SELECT name FROM stations WHERE id=?"); + q.bind(1, id); + if(q.step() == SQLITE_ROW) + return q.getRows().get(0); + return QString(); +} + +db_id StationsMatchModel::getIdAtRow(int row) const +{ + return items[row].stationId; +} + +QString StationsMatchModel::getNameAtRow(int row) const +{ + return items[row].name; +} + +void StationsMatchModel::setFilter(db_id lineId, db_id exceptStId) +{ + m_lineId = lineId; + m_exceptStId = exceptStId; + + QByteArray sql; + + if(m_lineId) + { + sql = "SELECT railways.stationId," + " stations.name" + " FROM railways" + " JOIN stations ON stations.id=railways.stationId" + " WHERE railways.lineId=?3 AND "; + if(m_exceptStId) + sql.append("railways.stationId<>?2 AND "); + }else{ + sql = "SELECT stations.id,stations.name FROM stations WHERE "; + if(m_exceptStId) + sql.append("stations.id<>?2 AND "); + } + + sql.append("(stations.name LIKE ?1 OR stations.short_name LIKE ?1) LIMIT " QT_STRINGIFY(MaxMatchItems + 1)); + + q_getMatches.prepare(sql.constData()); +} diff --git a/src/stations/stationsmatchmodel.h b/src/stations/stationsmatchmodel.h new file mode 100644 index 0000000..474bf77 --- /dev/null +++ b/src/stations/stationsmatchmodel.h @@ -0,0 +1,49 @@ +#ifndef STATIONSMATCHMODEL_H +#define STATIONSMATCHMODEL_H + +#include "utils/sqldelegate/isqlfkmatchmodel.h" + +#include "utils/types.h" + +#include +using namespace sqlite3pp; + +class StationsMatchModel : public ISqlFKMatchModel +{ + Q_OBJECT +public: + StationsMatchModel(database &db, QObject *parent = nullptr); + + // Basic functionality: + QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override; + + // ISqlFKMatchModel: + void autoSuggest(const QString& text) override; + virtual void refreshData() override; + QString getName(db_id id) const override; + + db_id getIdAtRow(int row) const override; + QString getNameAtRow(int row) const override; + + // StationsMatchModel: + void setFilter(db_id lineId, db_id exceptStId); + +private: + struct StationItem + { + db_id stationId; + QString name; + char padding[4]; + }; + StationItem items[MaxMatchItems]; + + database &mDb; + query q_getMatches; + + db_id m_lineId; + db_id m_exceptStId; + + QByteArray mQuery; +}; + +#endif // STATIONSMATCHMODEL_H diff --git a/src/stations/stationssqlmodel.cpp b/src/stations/stationssqlmodel.cpp new file mode 100644 index 0000000..2892b3b --- /dev/null +++ b/src/stations/stationssqlmodel.cpp @@ -0,0 +1,760 @@ +#include "stationssqlmodel.h" +#include "app/session.h" + +#include +#include + +#include +using namespace sqlite3pp; + +#include "utils/worker_event_types.h" +#include "utils/model_roles.h" +#include "utils/platform_utils.h" + +#include "lines/linestorage.h" + +#include + +class StationsSQLModelResultEvent : public QEvent +{ +public: + static constexpr Type _Type = Type(CustomEvents::StationsModelResult); + inline StationsSQLModelResultEvent() : QEvent(_Type) {} + + QVector items; + int firstRow; +}; + +//Error messages +static constexpr char +errorNameAlreadyUsedText[] = QT_TRANSLATE_NOOP("StationsSQLModel", + "The name %1 is already used by another station.
" + "Please choose a different name for each station."); +static constexpr char +errorShortNameAlreadyUsedText[] = QT_TRANSLATE_NOOP("StationsSQLModel", + "The name %1 is already used as short name for station %2.
" + "Please choose a different name for each station."); +static constexpr char +errorNameSameShortNameText[] = QT_TRANSLATE_NOOP("StationsSQLModel", + "Name and short name cannot be equal (%1)."); + +StationsSQLModel::StationsSQLModel(sqlite3pp::database &db, QObject *parent) : + IPagedItemModel(500, db, parent), + cacheFirstRow(0), + firstPendingRow(-BatchSize) +{ + sortColumn = NameCol; + LineStorage *lines = Session->mLineStorage; + connect(lines, &LineStorage::stationAdded, this, &StationsSQLModel::onStationAdded); + connect(lines, &LineStorage::stationRemoved, this, &StationsSQLModel::onStationRemoved); +} + +bool StationsSQLModel::event(QEvent *e) +{ + if(e->type() == StationsSQLModelResultEvent::_Type) + { + StationsSQLModelResultEvent *ev = static_cast(e); + ev->setAccepted(true); + + handleResult(ev->items, ev->firstRow); + + return true; + } + + return QAbstractTableModel::event(e); +} + +QVariant StationsSQLModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal) + { + switch (role) + { + case Qt::DisplayRole: + { + switch (section) + { + case NameCol: + return tr("Name"); + case ShortName: + return tr("Short Name"); + case Platforms: + return tr("Platforms"); + case Depots: + return tr("Depots"); + case PlatformColor: + return tr("Main Color"); + case DefaultFreightPlatf: + return tr("Freight Plaftorm"); + case DefaultPassengerPlatf: + return tr("Passenger Platf"); + } + break; + } + case Qt::ToolTipRole: + { + switch (section) + { + case DefaultFreightPlatf: + return tr("Default platform for newly created stops when job category is Freight, Postal or Engine movement"); + case DefaultPassengerPlatf: + return tr("Default platform for newly created stops when job is passenger train"); + } + break; + } + } + } + else if(role == Qt::DisplayRole) + { + return section + curPage * ItemsPerPage + 1; + } + + return QAbstractTableModel::headerData(section, orientation, role); +} + +int StationsSQLModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : curItemCount; +} + +int StationsSQLModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant StationsSQLModel::data(const QModelIndex &idx, int role) const +{ + const int row = idx.row(); + if (!idx.isValid() || row >= curItemCount || idx.column() >= NCols) + return QVariant(); + + 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 StationItem& item = cache.at(row - cacheFirstRow); + + switch (role) + { + case Qt::DisplayRole: + case Qt::EditRole: + { + switch (idx.column()) + { + case NameCol: + return item.name; + case ShortName: + return item.shortName; + case Platforms: + return item.platfs; + case Depots: + return item.depots; + case DefaultFreightPlatf: + return utils::shortPlatformName(item.defaultFreightPlatf); + case DefaultPassengerPlatf: + return utils::shortPlatformName(item.defaultPassengerPlatf); + } + break; + } + + case STATION_ID: + return item.stationId; + case COLOR_ROLE: + return item.platfColor; + case PLATF_ID: + { + switch (idx.column()) + { + case DefaultFreightPlatf: + return item.defaultFreightPlatf; + case DefaultPassengerPlatf: + return item.defaultPassengerPlatf; + } + } + } + + return QVariant(); +} + +bool StationsSQLModel::setData(const QModelIndex &idx, const QVariant &value, int role) +{ + const int row = idx.row(); + if(!idx.isValid() || row >= curItemCount || idx.column() >= NCols || row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return false; //Not fetched yet or invalid + + StationItem &item = cache[row - cacheFirstRow]; + QModelIndex first = idx; + QModelIndex last = idx; + + switch (role) + { + case Qt::EditRole: + { + switch (idx.column()) + { + case NameCol: + { + if(!setName(item, value.toString())) + return false; + break; + } + case ShortName: + { + if(!setShortName(item, value.toString())) + return false; + break; + } + case Platforms: + { + bool ok = false; + int val = value.toInt(&ok); + if(!ok || !setPlatfCount(item, val)) + return false; + break; + break; + } + case Depots: + { + bool ok = false; + int val = value.toInt(&ok); + if(!ok || !setDepotCount(item, val)) + return false; + break; + break; + } + case DefaultFreightPlatf: + { + bool ok = false; + int val = value.toInt(&ok); + if(!ok || !setDefaultFreightPlatf(item, val)) + return false; + break; + } + case DefaultPassengerPlatf: + { + bool ok = false; + int val = value.toInt(&ok); + if(!ok || !setDefaultPassengerPlatf(item, val)) + return false; + break; + } + } + break; + } + case COLOR_ROLE: + { + QRgb rgb = value.toUInt(); + if(item.platfColor == rgb) + return false; + + command q_setPlatfColor(mDb, "UPDATE stations SET platf_color=? WHERE id=?"); + q_setPlatfColor.bind(1, qint64(rgb)); + q_setPlatfColor.bind(2, item.stationId); + if(q_setPlatfColor.execute() != SQLITE_OK) + { + qDebug() << "Error setting color station:" << item.stationId << mDb.error_msg(); + return false; + } + item.platfColor = rgb; + emit Session->mLineStorage->stationColorChanged(item.stationId); + } + } + + emit dataChanged(first, last); + + return true; +} + +Qt::ItemFlags StationsSQLModel::flags(const QModelIndex &idx) const +{ + if (!idx.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren; + if(idx.row() < cacheFirstRow || idx.row() >= cacheFirstRow + cache.size()) + return f; //Not fetched yet + + f.setFlag(Qt::ItemIsEditable); + + return f; +} + +void StationsSQLModel::clearCache() +{ + cache.clear(); + cache.squeeze(); + cacheFirstRow = 0; +} + +void StationsSQLModel::refreshData() +{ + if(!mDb.db()) + return; + + emit itemsReady(-1, -1); //Notify we are about to refresh + + //TODO: consider filters + query q(mDb, "SELECT COUNT(1) FROM stations"); + 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 StationsSQLModel::setSortingColumn(int col) +{ + if(sortColumn == col || (col != NameCol && col != PlatformColor)) + return; + + clearCache(); + sortColumn = col; + + QModelIndex first = index(0, 0); + QModelIndex last = index(curItemCount - 1, NCols - 1); + emit dataChanged(first, last); +} + +bool StationsSQLModel::addStation(db_id *outStationId) +{ + return Session->mLineStorage->addStation(outStationId); +} + +bool StationsSQLModel::removeStation(db_id stationId) +{ + return Session->mLineStorage->removeStation(stationId); +} + +void StationsSQLModel::onStationAdded() +{ + refreshData(); //Recalc row count + setSortingColumn(NameCol); + switchToPage(0); //Reset to first page and so it is shown as first row +} + +void StationsSQLModel::onStationRemoved() +{ + refreshData(); //Recalc row count +} + +void StationsSQLModel::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; + + + //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 StationsSQLModel::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 = nullptr; + + QByteArray sql = "SELECT id,name,short_name,platforms,depot_platf,platf_color,defplatf_freight,defplatf_passenger FROM stations"; + switch (sortCol) + { + case NameCol: + { + whereCol = "name"; //Order by 1 column, no where clause + break; + } + case Platforms: + { + whereCol = "platforms,name"; + break; + } + } + + if(val.isValid()) + { + sql += " WHERE "; + sql += whereCol; + if(reverse) + 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); + +// if(val.isValid()) +// { +// switch (sortCol) +// { +// case LineNameCol: +// { +// q.bind(3, val.toString()); +// break; +// } +// } +// } + + QVector vec(BatchSize); + + auto it = q.begin(); + const auto end = q.end(); + + QRgb whiteColor = qRgb(255, 255, 255); + + if(reverse) + { + int i = BatchSize - 1; + + for(; it != end; ++it) + { + auto r = *it; + StationItem &item = vec[i]; + item.stationId = r.get(0); + item.name = r.get(1); + item.shortName = r.get(2); + item.platfs = r.get(3); + item.depots = r.get(4); + if(r.column_type(5) == SQLITE_NULL) + item.platfColor = whiteColor; + else + item.platfColor = QRgb(r.get(5)); + item.defaultFreightPlatf = r.get(6); + item.defaultPassengerPlatf = r.get(7); + i--; + } + if(i > -1) + vec.remove(0, i + 1); + } + else + { + int i = 0; + + for(; it != end; ++it) + { + auto r = *it; + StationItem &item = vec[i]; + item.stationId = r.get(0); + item.name = r.get(1); + item.shortName = r.get(2); + item.platfs = r.get(3); + item.depots = r.get(4); + if(r.column_type(5) == SQLITE_NULL) + item.platfColor = whiteColor; + else + item.platfColor = QRgb(r.get(5)); + item.defaultFreightPlatf = r.get(6); + item.defaultPassengerPlatf = r.get(7); + i++; + } + if(i < BatchSize) + vec.remove(i, BatchSize - i); + } + + + StationsSQLModelResultEvent *ev = new StationsSQLModelResultEvent; + ev->items = vec; + ev->firstRow = first; + + qApp->postEvent(this, ev); +} + +void StationsSQLModel::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 itemsReady(firstRow, lastRow); + + qDebug() << "TOTAL: From:" << cacheFirstRow << "To:" << cacheFirstRow + cache.size() - 1; +} + +bool StationsSQLModel::setName(StationsSQLModel::StationItem &item, const QString &val) +{ + const QString name = val.simplified(); + if(name.isEmpty() || item.name == name) + return false; + + //TODO: check non allowed characters + + query q(mDb, "SELECT id,name FROM stations WHERE short_name=?"); + q.bind(1, name); + if(q.step() == SQLITE_ROW) + { + db_id stId = q.getRows().get(0); + if(stId == item.stationId) + { + emit modelError(tr(errorNameSameShortNameText).arg(name)); + } + else + { + const QString otherShortName = q.getRows().get(1); + emit modelError(tr(errorShortNameAlreadyUsedText).arg(name, otherShortName)); + } + return false; + } + + q.prepare("UPDATE stations SET name=? WHERE id=?"); + q.bind(1, name); + q.bind(2, item.stationId); + int ret = q.step(); + if(ret != SQLITE_OK && ret != SQLITE_DONE) + { + if(ret == SQLITE_CONSTRAINT_UNIQUE) + { + emit modelError(tr(errorNameAlreadyUsedText).arg(name)); + } + else + { + emit modelError(tr("Error: %1").arg(mDb.error_msg())); + } + return false; + } + + item.name = name; + + //This row has now changed position so we need to invalidate cache + //HACK: we emit dataChanged for this index (that doesn't exist anymore) + //but the view will trigger fetching at same scroll position so it is enough + cache.clear(); + cacheFirstRow = 0; + + emit Session->mLineStorage->stationNameChanged(item.stationId); + + return true; +} + +bool StationsSQLModel::setShortName(StationsSQLModel::StationItem &item, const QString &val) +{ + const QString shortName = val.simplified(); + if(item.name == shortName) + return false; + + //TODO: check non allowed characters + + query q(mDb, "SELECT id,name FROM stations WHERE name=?"); + q.bind(1, shortName); + if(q.step() == SQLITE_ROW) + { + db_id stId = q.getRows().get(0); + if(stId == item.stationId) + { + emit modelError(tr(errorNameSameShortNameText).arg(shortName)); + } + else + { + const QString otherName = q.getRows().get(1); + emit modelError(tr(errorShortNameAlreadyUsedText).arg(shortName, otherName)); + } + return false; + } + + q.prepare("UPDATE stations SET short_name=? WHERE id=?"); + if(shortName.isEmpty()) + q.bind(1); //Bind NULL + else + q.bind(1, shortName); + q.bind(2, item.stationId); + int ret = q.step(); + if(ret != SQLITE_OK && ret != SQLITE_DONE) + { + if(ret == SQLITE_CONSTRAINT_UNIQUE) + { + emit modelError(tr(errorShortNameAlreadyUsedText).arg(shortName).arg(QString())); + } + else + { + emit modelError(tr("Error: %1").arg(mDb.error_msg())); + } + return false; + } + + item.shortName = shortName; + + emit Session->mLineStorage->stationNameChanged(item.stationId); + + return true; +} + +bool StationsSQLModel::setPlatfCount(StationsSQLModel::StationItem &item, int platf) +{ + if(item.platfs == platf || platf < 0 || (platf == 0 && item.depots == 0)) //If there are no depots there must be at least 1 main platform + return false; + item.platfs = qint8(platf); + + command q_setPlatf(mDb, "UPDATE stations SET platforms=? WHERE id=?"); + q_setPlatf.bind(1, item.platfs); + q_setPlatf.bind(2, item.stationId); + q_setPlatf.execute(); + q_setPlatf.reset(); + + //If ob.platfs == 0 -> they are set to -1 which is first depot platf + if(item.defaultFreightPlatf >= item.platfs) + setDefaultFreightPlatf(item, item.platfs - 1); + if(item.defaultPassengerPlatf >= item.platfs) + setDefaultPassengerPlatf(item, item.platfs - 1); + + emit Session->mLineStorage->stationModified(item.stationId); + + return true; +} + +bool StationsSQLModel::setDepotCount(StationsSQLModel::StationItem &item, int depots) +{ + if(item.depots == depots || depots < 0 || (depots == 0 && item.platfs == 0)) //If there are no depots there must be at least 1 main platform + return false; + item.depots = qint8(depots); + + command q_setPlatf(mDb, "UPDATE stations SET depot_platf=? WHERE id=?"); + q_setPlatf.bind(1, item.depots); + q_setPlatf.bind(2, item.stationId); + q_setPlatf.execute(); + q_setPlatf.reset(); + + //If ob.platfs == 0 -> they are set to -1 which is first depot platf + if(item.defaultFreightPlatf < -item.depots) + setDefaultFreightPlatf(item, -item.depots); + if(item.defaultPassengerPlatf < -item.depots) + setDefaultPassengerPlatf(item, -item.depots); + + emit Session->mLineStorage->stationModified(item.stationId); + + return true; +} + +bool StationsSQLModel::setDefaultFreightPlatf(StationsSQLModel::StationItem &item, int platf) +{ + if(platf >= item.platfs || platf < -item.depots) + return false; + + command cmd(mDb, "UPDATE stations SET defplatf_freight=? WHERE id=?"); + cmd.bind(1, platf); + cmd.bind(2, item.stationId); + if(cmd.execute() != SQLITE_OK) + return false; + item.defaultFreightPlatf = qint8(platf); + return true; +} + +bool StationsSQLModel::setDefaultPassengerPlatf(StationsSQLModel::StationItem &item, int platf) +{ + if(platf >= item.platfs || platf < -item.depots) + return false; + + command cmd(mDb, "UPDATE stations SET defplatf_passenger=? WHERE id=?"); + cmd.bind(1, platf); + cmd.bind(2, item.stationId); + if(cmd.execute() != SQLITE_OK) + return false; + item.defaultPassengerPlatf = qint8(platf); + return true; +} diff --git a/src/stations/stationssqlmodel.h b/src/stations/stationssqlmodel.h new file mode 100644 index 0000000..8761b0e --- /dev/null +++ b/src/stations/stationssqlmodel.h @@ -0,0 +1,127 @@ +#ifndef STATIONSSQLMODEL_H +#define STATIONSSQLMODEL_H + +#include "utils/sqldelegate/pageditemmodel.h" + +#include "utils/types.h" + +#include + +#include + +class StationsSQLModel : public IPagedItemModel +{ + Q_OBJECT +public: + + enum { BatchSize = 100 }; + + typedef enum { + NameCol = 0, + ShortName, + Platforms, + Depots, + PlatformColor, + DefaultFreightPlatf, + DefaultPassengerPlatf, + NCols + } Columns; + + typedef struct StationItem_ + { + db_id stationId; + QString name; + QString shortName; + QRgb platfColor; + + qint8 platfs; + qint8 depots; + qint8 defaultFreightPlatf; + qint8 defaultPassengerPlatf; + } StationItem; + + StationsSQLModel(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; + + // Editable: + bool setData(const QModelIndex &idx, const QVariant &value, + int role = Qt::EditRole) override; + + Qt::ItemFlags flags(const QModelIndex& idx) 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; + + // StationsSQLModel + bool addStation(db_id *outStationId = nullptr); + bool removeStation(db_id stationId); + + // Convinience + inline db_id getIdAtRow(int row) const + { + if (row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return 0; //Invalid + + const StationItem& item = cache.at(row - cacheFirstRow); + return item.stationId; + } + + inline QString getNameAtRow(int row) const + { + if (row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return QString(); //Invalid + + const StationItem& item = cache.at(row - cacheFirstRow); + return item.name; + } + + inline QPair getPlatfCountAtRow(int row) const + { + if (row < cacheFirstRow || row >= cacheFirstRow + cache.size()) + return {-1, -1}; //Invalid + + const StationItem& item = cache.at(row - cacheFirstRow); + return {item.platfs, item.depots}; + } + +private slots: + void onStationAdded(); + void onStationRemoved(); + +private: + void fetchRow(int row); + Q_INVOKABLE void internalFetch(int first, int sortCol, int valRow, const QVariant &val); + void handleResult(const QVector &items, int firstRow); + + bool setName(StationItem &item, const QString &val); + bool setShortName(StationItem &item, const QString &val); + bool setPlatfCount(StationItem &item, int platf); + bool setDepotCount(StationItem &item, int depots); + bool setDefaultFreightPlatf(StationItem &item, int platf); + bool setDefaultPassengerPlatf(StationItem &item, int platf); + +private: + QVector cache; + int cacheFirstRow; + int firstPendingRow; +}; + +#endif // STATIONSSQLMODEL_H diff --git a/src/translations/CMakeLists.txt b/src/translations/CMakeLists.txt new file mode 100644 index 0000000..766f012 --- /dev/null +++ b/src/translations/CMakeLists.txt @@ -0,0 +1,7 @@ +set_directory_properties(PROPERTIES CLEAN_NO_CUSTOM TRUE) + +set(TRAINTIMETABLE_TS_FILES + translations/traintimetable_it.ts + translations/traintimetable_de.ts + PARENT_SCOPE +) diff --git a/src/translations/traintimetable_de.ts b/src/translations/traintimetable_de.ts new file mode 100644 index 0000000..d98f8c0 --- /dev/null +++ b/src/translations/traintimetable_de.ts @@ -0,0 +1,3816 @@ + + + + + ChooseItemDlg + + + Insert value + + + + + Error + + + + + In order to proceed you must select a valid item. + + + + + ColorView + + + Choose a color + + + + + DirectionNames + + + Left + + + + + Right + + + + + EditStopDialog + + + Stop + + + + + + Time + + + + + + Arrival + + + + + Station + + + + + + Departure + + + + + Coupled + + + + + + Edit + + + + + Uncoupled + + + + + Platform + + + + + Main + + + + + Depot + + + + + Description + + + + + Previous Station + + + + + Line Max.Speed + + + + + Speed + + + + + Train Max.Speed + + + + + Re-Calc + + + + + Apply + + + + + + + Km/h + + + + + Line + + + + + Info + + + + + Infos here... + + + + + Rollingstock + + + + + Asset After Stop + + + + + Asset Before Stop + + + + + Passings / Crossings + + + + + + Refresh + + + + + Passings + + + + + Crossings + + + + + This is the first stop + + + + + Train leaves %1 at %2 + + + + + +This is the last stop + + + + + Couple + + + + + Uncouple + + + + + No Engine Left + + + + + It seems you have uncoupled all job engines except for electric ones but the line is not electrified +(The train isn't able to move) +Do you want to couple a non electric engine? + + + + + It seems you have uncoupled all job engines +(The train isn't able to move) +Do you want to couple an engine? + + + + + Train Speed Changed + + + + + Train speed after this stop has changed from a value of %1 km/h to <b>%2 km/h</b> +Do you want to rebase travel times to this new speed? +NOTE: this doesn't affect stop times but you will lose manual adjustments to travel times + + + + + FileFormats + + + All Files (*.*) + + + + + OpenDocument Sheet (*.ods) + + + + + OpenDocument Text (*.odt) + + + + + Train Timetable Session (*.ttt) + + + + + SQLite 3 Database (*.db *.sqlite *.sqlite3 *.db3) + + + + + SVG vectorial image (*.svg) + + + + + Portable Document Format (*.pdf) + + + + + FileOptionsPage + + + File options + + + + + Files + + + + + Different Files + + + + + Choose + + + + + File(s) + + + + + Choose Folder + + + + + Choose file + + + + + FixDuplicatesDlg + + + Loaded. + + + + + Starting... + + + + + Counting items... + + + + + Loading data... + + + + + Unknown state. + + + + + Form + + + Files + + + + + Choose + + + + + Different Files + + + + + GraphOptions + + + Select All + + + + + Unselect All + + + + + Hide same station at job start + + + + + Shifts Graph Options + + + + + ISqlFKMatchModel + + + + + + + + Empty + + + + + ImageViewer + + + No image + + + + + JobCategoryName + + + FREIGHT + + + + + + LIS + + + + + POSTAL + + + + + REGIONAL + + + + + FAST REGIONAL + + + + + LOCAL + + + + + INTERCITY + + + + + EXPRESS + + + + + DIRECT + + + + + HIGH SPEED + + + + + FRG + + + + + P + + + + + R + + + + + RF + + + + + LOC + + + + + IC + + + + + EXP + + + + + DIR + + + + + HSP + + + + + JobChangeShiftDlg + + + Shift Error + + + + + Error while setting shift <b>%1</b> to job <b>%2</b>.<br>Msg: %3 + + + + + Change shift for job <b>%1</b>: + + + + + JobPassingsModel + + + Job + + + + + Arrival + + + + + Departure + + + + + Platform + + + + + JobPathEditor + + + Save Sheet + + + + + Invalid Operation + + + + + Transit cannot have coupling or uncoupling operations. +Remove theese operation to set Transit on this stop. + + + + + Toggle transit + + + + + Set transit + + + + + Unset transit + + + + + Remove + + + + + Insert before + + + + + Edit stop + + + + + Error + + + + + You must register at least 2 stops. +Do you want to delete this job? + + + + + Save? + + + + + Do you want to save changes to job %1 + + + + + Invalid + + + + + Job number <b>%1</b> is already exists.<br>Please choose a different number. + + + + + Empty Job + + + + + Before setting a shift you should add stops to this job + + + + + Save Job Sheet + + + + + job%1_sheet.odt + + + + + JobsManager + + + New Job + + + + + Remove + + + + + Remove All + + + + + Job deletion + + + + + Are you sure to delete job %1? + + + + + Delete all jobs? + + + + + Are you really sure you want to delete all jobs from this session? + + + + + JobsSQLModel + + + Number + + + + + Category + + + + + Shift + + + + + Origin + + + + + Departure + + + + + Destination + + + + + Arrival + + + + + LinesSQLModel + + + Name + + + + + Max. Speed + + + + + Type + + + + + %1 km/h + + + + + LoadTaskUtils + + + %1 +Code: %2 +Message: %3 + + + + + Could not open session file correctly. + + + + + Query preparation failed A1. + + + + + Query preparation failed A2. + + + + + Query preparation failed B1. + + + + + Query preparation failed B2. + + + + + + Query preparation failed C1. + + + + + + Query preparation failed C2. + + + + + MainWindow + + + File + + + + + Edit + + + + + Help + + + + + View + + + + + 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 + + + + + Next Job Segment + + + + + Prev Job Segment + + + + + Properties + + + + + Session Rollingstock Summary + + + + + Show rollingstock position at start or end of session + + + + + Meeting Information + + + + + About %1 + + + + + Choose line + + + + + Choose a railway line + + + + + Find + + + + + %1 application makes easier to deal with timetables and trains +Beta version: %2 +Built: %3 + + + + + + Backgroung Task + + + + + + Background task for checking rollingstock errors is still running. +Do you want to cancel it? + + + + + Open Session + + + + + + Background Tasks + + + + + Some background tasks are still running. +The file was not opened. Try again. + + + + + Version is old + + + + + This file was created by an older version of Train Timetable. +Opening it without conversion might not work and even crash the application. +Do you want to open it anyway? + + + + + Version is too new + + + + + This file was created by a newer version of Train Timetable. +You should update the application first. Opening this file might not work or even crash. +Do you want to open it anyway? + + + + + RS Import + + + + + There is some rollingstock import data left in this file. Probably the application has crashed!<br>Before deleting it would you like to resume importation?<br><i>(Sorry for the crash, would you like to contact me and share information about it?)</i> + + + + + Resume importation + + + + + Just delete it + + + + + &%1 %2 + + + + + Create new Session + + + + + Some background tasks are still running. +The new file was not created. Try again. + + + + + Save Session Copy + + + + + Error saving copy + + + + + Error while Closing + + + + + There was an error while closing the database. +Make sure there aren't any background tasks running and try again. + + + + + <p>Open a file: <b>File</b> > <b>Open</b></p><p>Create new project: <b>File</b> > <b>New</b></p> + + + + + Open file or create a new one + + + + + <p><b>There are no lines in this session</b></p><p><table align="center"><tr><td>Start by creating the railway layout for this session:</td></tr><tr><td><table><tr><td>1.</td><td>Create stations (<b>Edit</b> > <b>Stations</b>)</td></tr><tr><td>2.</td><td>Create railway lines (<b>Edit</b> > <b>Stations</b> > <b>Lines Tab</b>)</td></tr><tr><td>3.</td><td>Add stations to railway lines</td></tr><tr><td></td><td>(<b>Edit</b> > <b>Stations</b> > <b>Lines Tab</b> > <b>Edit Line</b>)</td></tr></table></td></tr></table></p> + + + + + Add train job + + + + + You must create at least one line before adding job to this session + + + + + MeetingInformationDialog + + + Meeting Information + + + + + Meeting + + + + + Meeting end day: + + + + + City: + + + + + Hosting association: + + + + + Meeting start day: + + + + + Picture Logo + + + + + View picture + + + + + Import Picture + + + + + Remove Picture + + + + + Sheet export + + + + + + + Reset + + + + + Footer text: + + + + + Header text: + + + + + Show meeting dates on first page + + + + + Meeting description + + + + + Database Error + + + + + This database doesn't support metadata. +Make sure it was created by a recent version of the application and was not manipulated. + + + + + Set custom text + + + + + Import image + + + + + Importing error + + + + + The image format is not supported or the file is corrupted. + + + + + Remove image? + + + + + Are you sure to remove the image logo? + + + + + MergeModelsDialog + + + Merge Models + + + + + Remove model A + + + + + Merge model A in B: all RS of model A will be changed to model B + + + + + + Invalid + + + + + Invalid Models + + + + + Models must not be null and must be different + + + + + Merging completed + + + + + Models merged succesfully. + + + + + Error while merging + + + + + Some error occurred while merging models. + + + + + No model set + + + + + Error + + + + + %1Axes: <b>%2</b> Max.Speed: <b>%3 km/h</b><br>Type: <b>%4</b> + + + + + MergeOwnersDialog + + + Merge Owners + + + + + Merge owner A in B: all RS of owner A will be changed to owner B + + + + + Remove Owner A + + + + + Invalid Owners + + + + + Owners must not be null and must be different + + + + + Merging completed + + + + + Owners merged succesfully. + + + + + Error while merging + + + + + Some error occurred while merging owners. + + + + + ModelPageSwitcher + + + Prev + + + + + Next + + + + + Go + + + + + Refresh + + + + + Page %1/%2 (%3 items) + + + + + ODSOptionsWidget + + + Import rollingstock pieces, models and owners from a spreadsheet file. +The file must be a valid Open Document Format Spreadsheet V1.2 +Extension: (*.ods) + + + + + First non-empty row that contains rollingstock piece information + + + + + Column from which item number is extracted + + + + + Column from which item model name is extracted + + + + + Odt + + + From: + + + + + Departure: + + + + + To: + + + + + Arrival: + + + + + Axes: + + + + + + + Station + + + + + + + Arrival + + + + + + + Departure + + + + + + + Platf + + + + + + + Rollingstock + + + + + + Crossings + + + + + + Passings + + + + + + Notes + + + + + Meeting in %1 from %2 to %3 + + + + + Meeting in %1 on %2 + + + + + Meeting + + + + + Page + + + + + Cp: + + + + + Unc: + + + + + Job + + + + + + Owner + + + + + Rollingstock by %1 at %2 of session + + + + + start + + + + + end + + + + + Station: %1%2 + + + + + (%1) + + + + + Job N + + + + + From + + + + + To + + + + + Transit + + + + + Shift %1 + + + + + From %1 to %2 + + + + + SHIFT %1 + + + + + %1 station + + + + + OptionsPage + + + Default speed is applied when a rollingstock model is not matched to an existing one and has to be created from scratch + + + + + Default speed + + + + + Default type is applied when a rollingstock model is not matched to an existing one and has to be created from scratch + + + + + Default type + + + + + PrintWizard + + + Print Wizard + + + + + PrinterOptionsPage + + + Open settings + + + + + Printer + + + + + Printer options + + + + + ProgressPage + + + Printing... + + + + + Printing + + + + + Completed + + + + + PropertiesDialog + + + File Properties + + + + + File Path: + + + + + No opened file + + + + + RSCoupleDialog + + + Engines + + + + + Coaches + + + + + Freight Wagons + + + + + <p style="font-size:13pt"><span style="background-color:#FF56FF">___</span> The item isn't coupled before or already coupled.<br><span style="background-color:#FF3d43">___</span> The item isn't in this station.<br><span style="color:#0000FF;background-color:#FFFFFF">\\\\</span> Railway line doesn't allow electric traction.<br><span style="background-color:#00FFFF">___</span> First use of this item.<br><span style="background-color:#00FF00">___</span> This item is never used in this session.</p> + + + + + Hide legend + + + + + Show legend + + + + + RSCouplingInterface + + + + Error + + + + + Error while adding coupling operation. +Rollingstock %1 is already coupled by this job (%2) + + + + + Error while adding coupling operation. +Rollingstock %1 is already coupled to another job (%2) +Do you still want to couple it? + + + + + Warning + + + + + Rollingstock %1 is an Electric engine but the line is not electrified +This engine will not be albe to move a train. +Do you still want to couple it? + + + + + Delete coupling? + + + + + You couple %1 also in a next stop in %2 at %3. +Do you want to remove the other coupling operation? + + + + + + Delete uncoupling? + + + + + You don't couple %1 anymore. +Do you want to remove also the uncoupling operation in %2 at %3? + + + + + You uncouple %1 also in %2 at %3. +Do you want to remove the other uncoupling operation? + + + + + RSImportWizard + + + Completed. + + + + + RSImportedModelsModel + + + Models with empty name must have a Custom Name or must be matched to an existing model + + + + + You cannot set the same name in the 'Custom Name' field. +If you meant to revert to original name then clear the custom name and leave the cell empty + + + + + There is already an existing Model with same name: <b>%1</b> +If you meant to merge theese rollingstock pieces with this existing model please use 'Match Existing' field + + + + + There is already an imported Model with name: <b>%1</b> +If you meant to merge theese rollingstock pieces with this existing model after importing rollingstock use the merge tool to merge them + + + + + You already gave the same custom name: <b>%1</b> to the imported model: <b>%2</b> +In order to proceed you need to assign a different custom name to %2 + + + + + You already gave the same custom name: <b>%1</b> to the imported model: <b>%2</b> +Please choose a different name or leave empty for the original name + + + + + RSImportedOwnersModel + + + Owners with empty name must have a Custom Name or must be matched to an existing owner + + + + + You cannot set the same name in the 'Custom Name' field. +If you meant to revert to original name then clear the custom name and leave the cell empty + + + + + There is already an existing Owner with same name: <b>%1</b> +If you meant to merge theese rollingstock pieces with this existing owner please use 'Match Existing' field + + + + + There is already an imported Owner with name: <b>%1</b> +If you meant to merge theese rollingstock pieces with this existing owner after importing rollingstock use the merge tool to merge them + + + + + You already gave the same custom name: <b>%1</b> to the imported owner: <b>%2</b> +In order to proceed you need to assign a different custom name to %2 + + + + + You already gave the same custom name: <b>%1</b> to the imported owner: <b>%2</b> +Please choose a different name or leave empty for the original name + + + + + RSImportedRollingstockModel + + + You cannot set the same name in the 'Custom Name' field. +If you meant to revert to original name then clear the custom name and leave the cell empty + + + + + There is already another imported rollingstock with same 'New Number': <b>%1 %2</b> + + + + + There is already another imported rollingstock with same number: <b>%1 %2</b> + + + + + There is already an existing rollingstock with same number: <b>%1 %2</b> + + + + + RSJobViewer + + + Update + + + + + Show in Job Editor + + + + + Type: <b>%1</b><br>Owner: <b>%2</b> + + + + + Not set! + + + + + RSModelsSQLModel + + + This model name (<b>%1</b>) is already used with the same suffix (<b>%2</b>). +If you intend to create a new model of same name but different suffix, please first set the suffix. + + + + + This model suffix (<b>%1</b>) is already used with the same name (<b>%2</b>). + + + + + Rollingstock maximum speed must be > 0 km/h. + + + + + Rollingstock must have at least 2 axes. + + + + + There are rollingstock pieces of model <b>%1</b> so it cannot be removed. +If you wish to remove it, please first delete all <b>%1</b> pieces. + + + + + Suffix is already used. Suffix must be different among models of same name. + + + + + RSOwnersSQLModel + + + This owner name (<b>%1</b>) is already used. + + + + + There are rollingstock pieces of owner <b>%1</b> so it cannot be removed. +If you wish to remove it, please first delete all <b>%1</b> pieces. + + + + + RSProxyModel + + + Rollingstock <b>%1</b> cannot be uncoupled here because it wasn't coupled to this job before this stop or because it was already uncoupled before this stop.<br>Please remove the tick + + + + + Rollingstock <b>%1</b> cannot be coupled here because it was already coupled to this job before this stop<br>Please remove the tick + + + + + Rollingstock <b>%1</b> cannot be coupled here because it was already coupled before this stop<br>to job <b>%2<b/><br>Please remove the tick + + + + + Rollingstock <b>%1</b> cannot be coupled here because it is not in this station.<br>Please remove the tick + + + + + Engine <b>%1</b> is electric but the line is not electrified! + + + + + Rollingstock <b>%1</b> is coupled in this station also by <b>%2</b> at <b>%3</b>. + + + + + Rollingstock <b>%1</b> was left in this station by <b>%2</b> at <b>%3</b>. + + + + + This is the first use of this rollingstock <b>%1</b> + + + + + This would be the first use of this rollingstock <b>%1</b> + + + + + Rollingstock <b>%1</b> is never used in this session. You can couple it for the first time from any one station + + + + + RailwayNodeEditor + + + Add + + + + + Remove + + + + + Please choose a line for the new entry + + + + + Line + + + + + Please choose a station for the new entry + + + + + Station + + + + + You must select a valid line. + + + + + You must select a valid station. + + + + + You cannot add the same line twice to the same station. + + + + + You cannot add the same station twice to the same line. + + + + + RailwayNodeModel + + + Rollingstock item <b>%1</b> is used in some jobs so it cannot be removed. +If you wish to remove it, please first remove it from its jobs. + + + + + Line + + + + + Station + + + + + Km + + + + + Direction + + + + + RollingStockManager + + + Rollingstock Manager + + + + + New Rollingstock + + + + + + + Remove + + + + + View Plan + + + + + Search rollingstock item + + + + + Delete All Rollingstock + + + + + Import + + + + + Session Summary + + + + + New Model + + + + + Merge Models + + + + + New with suffix + + + + + Search rollingstock model + + + + + Delete All Models + + + + + New Owner + + + + + Merge Owners + + + + + Delete All Owners + + + + + Please choose a rollingstock item + + + + + [model][.][number][:owner] + + + + + Error adding rollingstock piece + + + + + Delete All Rollingstock? + + + + + Are you really sure you want to delete all rollingstock from this session? +NOTE: this will not erease model and owners, just rollingstock pieces. + + + + + + + + Error + + + + + Failed to remove rollingstock. +Make sure there are no more couplings in this session. +NOTE: you can remove all jobs at once from the Jobs Manager. + + + + + Error adding model + + + + + Please choose a rollingstock model + + + + + Model + + + + + You must select a valid rollingstock model. + + + + + Please choose an unique suffix for this model, or leave empty + + + + + Choose Suffix + + + + + Delete All Rollingstock Models? + + + + + Are you really sure you want to delete all rollingstock models from this session? +NOTE: this can be done only if there are no rollingstock pieces in this session. + + + + + Failed to remove rollingstock models. +Make sure there are no more rollingstock pieces in this session. +NOTE: you can remove all rollinstock pieces at once from the Rollingstock tab. + + + + + Delete All Rollingstock Owners? + + + + + Are you really sure you want to delete all rollingstock owners from this session? +NOTE: this can be done only if there are no rollingstock pieces in this session. + + + + + Failed to remove rollingstock owners. +Make sure there are no more rollingstock pieces in this session. +NOTE: you can remove all rollingstock pieces at once from the Rollingstock tab. + + + + + RollingstockSQLModel + + + Rollingstock item <b>%1</b> is used in some jobs so it cannot be removed.<br>If you wish to remove it, please first remove it from its jobs. + + + + + RsErrorTreeModel + + + Job + + + + + Station + + + + + Arrival + + + + + Description + + + + + RsErrors + + + Stop is transit. Cannot couple/uncouple rollingstock. + + + + + Coupled while busy: it was already coupled to another job. + + + + + Uncoupled when not coupled. + + + + + Not uncoupled at the end of the job or coupled by another job before this jobs uncouples it. + + + + + Coupled in a different station than that where it was uncoupled. + + + + + Uncoupled in the same stop it was coupled. + + + + + RsErrorsWidget + + + Start + + + + + Stop + + + + + Rollingstock Errors + + + + + Show in Job Editor + + + + + Show rollingstock plan + + + + + RsImportStrings + + + + + + + + Import + + + + + + Sheet No. + + + + + + + Name + + + + + + + Custom Name + + + + + + Model + + + + + + Number + + + + + + New number + + + + + + Owner + + + + + + Match Existing + + + + + Insert path here or click 'Choose' button + + + + + Choose + + + + + Choose file + + + + + Choose a file to import in *.ods format + + + + + File doesn't exist + + + + + Could not find file '%1' + + + + + Open Spreadsheet + + + + + Open Session + + + + + The file constains some duplicates in item names wich need to be fixed in order to procced. +There also may be some items with empty name. +Please assign a custom name to them so that there are no duplicates + + + + + Fix Item Names + + + + + + + Invalid Operation + + + + + Not yet! + + + + + There are still %1 items to be fixed + + + + + + Previuos page + + + + + Aborting RS Import + + + + + If you don't fix duplicated items you cannot proceed. +Do you wish to Abort the process? + + + + + Select owners of rollingstock you want to import + + + + + Select models of rollingstock you want to import + + + + + Select rollingstock pieces you want to import + + + + + You must select at least 1 owner + + + + + You must select at least 1 model + + + + + You must select at least 1 rollingsock piece + + + + + %1 owners selected + + + + + %1 models selected + + + + + %1 rollingstock pieces selected + + + + + General options + + + + + Import rollingstick owners + + + + + Import rollingstick models + + + + + Import rollingstick pieces + + + + + Import options + + + + + Import source: + + + + + Invalid option + + + + + You must at least import owners or models + + + + + No rolloingstock imported + + + + + No rollingstock piece will be imported. +In order to import rollingstock pieces you must also import models and owners. + + + + + In order to import rollingstock pieces you must also import models and owners. + + + + + File loading + + + + + Parsing file data... + + + + + Importing + + + + + Importing data... + + + + + Original + + + + + Abort import? + + + + + Do you want to import process? No data will be imported + + + + + + Aborting... + + + + + Loading Error + + + + + Error + + + + + Invalid option selected. Please try again. + + + + + RsPlanModel + + + Job + + + + + Station + + + + + Arrival + + + + + Departure + + + + + Operation + + + + + Coupled + + + + + Uncoupled + + + + + RsTypeNames + + + + + Suffix + + + + + + Max Speed + + + + + Axes + + + + + + + Type + + + + + Sub Type + + + + + + Owners + + + + + + Models + + + + + + Rollingstock + + + + + Model + + + + + Number + + + + + Owner + + + + + + Name + + + + + N. Axes + + + + + Subtype + + + + + Engine + + + + + Freight Wagon + + + + + Coach + + + + + Not set! + + + + + Electric + + + + + Diesel + + + + + Steam + + + + + SQLViewer + + + Query Error + + + + + SQLite Error: %1 +Extended: %2 +Message: %3 + + + + + Preparation Failed + + + + + SQLiteOptionsWidget + + + Import rollingstock pieces, models and owners from another Train Timetable session file. +The file must be a valid Train Timetable session of recent version +Extension: (*.ttt) + + + + + SelectionPage + + + Select All + + + + + Unselect All + + + + + Selection page + + + + + Select one or more lines to be printed + + + + + SessionStartEndModel + + + Rollingstock + + + + + Job + + + + + Platform + + + + + Departure + + + + + Arrival + + + + + Owner + + + + + Station + + + + + SessionStartEndRSViewer + + + Show Session Start + + + + + Show Session End + + + + + Order By Station + + + + + Order By Owner + + + + + Export Sheet + + + + + Rollingstock Summary + + + + + Expoert RS session plan + + + + + SettingsDialog + + + + + Settings + + + + + General + + + + + Language + + + + + Language changes will be applied next time you start the application + + + + + Restore Default Settings + + + + + Job Graph + + + + + + Offsets + + + + + Stations + + + + + + Hours + + + + + + Platforms + + + + + + Horizontal + + + + + + Vertical + + + + + Hour Line Start + + + + + Line Width + + + + + + Job + + + + + Hour + + + + + Colors + + + + + Hour Line + + + + + Hour Text + + + + + Main Platform + + + + + Depot Platform + + + + + Station Text + + + + + Stop + + + + + Default Stop Duration + + + + + Job Editor + + + + + Auto insert transits between two stops. + + + + + Choose next line before editing newly added last stop. + + + + + Auto uncouple all rollingstock items at last stop. + + + + + Auto move uncoupled rollingstock pieces from last stop when adding a new one. + + + + + Shift Graph + + + + + Job Box + + + + + Stations Labels + + + + + Hide Same Stations + + + + + Rollingstock + + + + + Merge models + + + + + Remove source merged model by default + + + + + Merge owners + + + + + Remove sourcemerged owner by default + + + + + Open Document Spreadsheet Import + + + + + First non-empty row + + + + + Number column + + + + + Model column + + + + + Sheet Export + + + + + Description + + + + + Header + + + + + Footer + + + + + Metadata + + + + + Store meeting location and dates in sheet metadata + + + + + Background Tasks + + + + + Rollingstock Error Checker + + + + + Check rollingstock when opening a file + + + + + Check rollingstock when a Job is edeited + + + + + minutes + + + + + Job Colors + + + + + Non Passenger + + + + + Passenger + + + + + Do you want to save settings? + + + + + Do you want to restore default settings? + + + + + ShiftBusyDlg + + + Shift is busy + + + + + Cannot set shift <b>%1</b> to job <b>%2</b>.<br>The selected shift is busy:<br>From: %3 To: %4 + + + + + ShiftBusyModel + + + Job + + + + + From + + + + + To + + + + + ShiftGraphEditor + + + Save + + + + + Print + + + + + Options + + + + + Refresh + + + + + Shift Graph Editor + + + + + Save Shift Graph + + + + + ShiftGraphHolder + + + + Shift %1 + + + + + ShiftJobsModel + + + Job + + + + + Departure + + + + + Origin + + + + + Arrival + + + + + Destination + + + + + ShiftManager + + + New + + + + + Remove + + + + + View Shift + + + + + Sheet + + + + + Graph + + + + + Shift Manager + + + + + Error Adding Shift + + + + + An error occurred while adding a new shift: +%1 + + + + + Save Shift Sheet + + + + + shift_%1.odt + + + + + ShiftSQLModel + + + Shift Name + + + + + There is already another job shift with same name: <b>%1</b> + + + + + ShiftViewer + + + Show in Job Editor + + + + + StationFreeRSModel + + + Name + + + + + Free from + + + + + Up to + + + + + Job A + + + + + Job B + + + + + StationFreeRSViewer + + + Refresh + + + + + Time: + + + + + Previous Operation + + + + + Next Operation + + + + + Free Rollingstock in %1 + + + + + + Error + + + + + + Database error. Try again. + + + + + + No Operation Found + + + + + No operation found in station %1 after %2! + + + + + No operation found in station %1 before %2! + + + + + Show RS Plan + + + + + Show Job A in JobEditor + + + + + Show Job B in JobEditor + + + + + StationJobView + + + Update + + + + + Save sheet + + + + + Show in Job Editor + + + + + Show job in graph + + + + + Save Station Sheet + + + + + %1_station.odt + + + + + StationLinesListModel + + + Name + + + + + StationPlanModel + + + Arrival + + + + + + Departure + + + + + Platform + + + + + Job + + + + + Notes + + + + + Transit + + + + + StationsManager + + + Stations + + + + + Lines + + + + + Stations Manager + + + + + + Add + + + + + + Remove + + + + + Plan + + + + + Free RS + + + + + + Edit + + + + + Error Adding Station + + + + + An error occurred while adding a new station: +%1 + + + + + Station error + + + + + Error Adding Line + + + + + An error occurred while adding a new line: +%1 + + + + + StationsSQLModel + + + The name <b>%1</b> is already used by another station.<br>Please choose a different name for each station. + + + + + The name <b>%1</b> is already used as short name for station <b>%2</b>.<br>Please choose a different name for each station. + + + + + Name and short name cannot be equal (<b>%1</b>). + + + + + Name + + + + + Short Name + + + + + Platforms + + + + + Depots + + + + + Main Color + + + + + Freight Plaftorm + + + + + Passenger Platf + + + + + Default platform for newly created stops when job category is Freight, Postal or Engine movement + + + + + Default platform for newly created stops when job is passenger train + + + + + + Error: %1 + + + + + StopDelegate + + + Line: %1 + + + + + StopEditor + + + Station name + + + + + Press shift if you don't want to change also departure time. + + + + + Previous page + + + + + Next page + + + + + Page: %1/%2 + + + + diff --git a/src/translations/traintimetable_it.ts b/src/translations/traintimetable_it.ts new file mode 100644 index 0000000..660cf30 --- /dev/null +++ b/src/translations/traintimetable_it.ts @@ -0,0 +1,3869 @@ + + + + + ChooseItemDlg + + + Insert value + Inserire valore + + + + Error + Errore + + + + In order to proceed you must select a valid item. + Per procedere devi selezionare un elemento valido. + + + + ColorView + + + Choose a color + Scegli un colore + + + + DirectionNames + + + Left + Sinistra + + + + Right + Destra + + + + EditStopDialog + + + Stop + Fermata + + + + + Time + Orari + + + + + Arrival + Arrivo + + + + Station + Stazione + + + + + Departure + Partenza + + + + Coupled + Agganciati + + + + + Edit + Modifica + + + + Uncoupled + Sganciati + + + + Platform + Binario + + + + Main + Pricipale + + + + Depot + Deposito + + + + Description + Descrizione + + + + Previous Station + Stazione Precedente + + + + Line Max.Speed + Velocità Max. Linea + + + + Speed + Velocità + + + + Train Max.Speed + Velocità Mx. Treno + + + + Re-Calc + Ricalcola + + + + Apply + Applica + + + + + + Km/h + + + + + Line + Linea + + + + Info + Informazioni + + + + Infos here... + Info... + + + + Rollingstock + Rotabili + + + + Asset Before Stop + Assetto prima della fermata + + + + Passings / Crossings + Precedenze / Incroci + + + + Passings + Precedenze + + + + Crossings + Incroci + + + + Asset After Stop + Assetto dopo la fermata + + + + + Refresh + Ricarica + + + + This is the first stop + Questa è la prima fermata + + + + Train leaves %1 at %2 + Il treno parte da %1 alle %2 + + + + +This is the last stop + +Questa è l'ultima fermata + + + + Couple + Aggancia + + + + Uncouple + Sgancia + + + + No Engine Left + Nessuna Locomotiva Rimasta + + + + It seems you have uncoupled all job engines except for electric ones but the line is not electrified +(The train isn't able to move) +Do you want to couple a non electric engine? + Sembra che tu abbia sganciato tutte le locomotive di questo servizio tranne quelle elettriche ma la linea non è elettrificata +(Il treno non si può muovere) +Vuoi agganciare una locomotiva non elettrica? + + + + It seems you have uncoupled all job engines +(The train isn't able to move) +Do you want to couple an engine? + Sembra che tu abbia sganciato tutte le locomotive di questo servizio +(Il treno non si può muovere) +Vuoi lasciarne una agganciata? + + + + Train Speed Changed + La velocità del treno è cambiata + + + + Train speed after this stop has changed from a value of %1 km/h to <b>%2 km/h</b> +Do you want to rebase travel times to this new speed? +NOTE: this doesn't affect stop times but you will lose manual adjustments to travel times + La velocità del treno dopo questa fermata è cambiata da un valore di %1 km/h a <b>%2 km/h</b> +Vuoi ricalcolare i tempi di percorrenza con questa nuova velocità? +NOTA: i tempi di sosta non sono affetti da questa modifica ma perderai gli aggiustamenti manuali fatti ai tempi di percorrenza + + + + FileFormats + + + All Files (*.*) + Tutti i file (*.*) + + + + OpenDocument Sheet (*.ods) + + + + + OpenDocument Text (*.odt) + + + + + Train Timetable Session (*.ttt) + + + + + SQLite 3 Database (*.db *.sqlite *.sqlite3 *.db3) + + + + + SVG vectorial image (*.svg) + + + + + Portable Document Format (*.pdf) + + + + + FileOptionsPage + + + File options + Opzioni Files + + + + Files + Files + + + + Different Files + Files differenti + + + + Choose + Scegli + + + + File(s) + + + + + Choose Folder + Scegli cartella + + + + Choose file + Scegli file + + + + FixDuplicatesDlg + + + Loaded. + Caricamento completato. + + + + Starting... + Inizializzazione... + + + + Counting items... + Conteggio elementi... + + + + Loading data... + Caricamento dati... + + + + Unknown state. + Stato non noto. + + + + Form + + + Files + + + + + Choose + Scegli + + + + Different Files + Files differenti + + + + GraphOptions + + + Select All + Seleziona Tutto + + + + Unselect All + Deseleziona Tutto + + + + Hide same station at job start + Nascondi stessa stazione alla partenza del servizio + + + + Shifts Graph Options + Opzioni Grafico Turni + + + + ISqlFKMatchModel + + + + + + + + Empty + Vuoto + + + + ImageViewer + + + No image + Nessuna immagine + + + + JobCategoryName + + + FREIGHT + MERCI + + + + + LIS + LIS + + + + POSTAL + POSTALE + + + + REGIONAL + REGIONALE + + + + FAST REGIONAL + REGIONALE VELOCE + + + + LOCAL + LOCALE + + + + INTERCITY + INTERCITY + + + + EXPRESS + ESPRESSO + + + + DIRECT + DIRETTO + + + + HIGH SPEED + ALTA VELOCITA' + + + + FRG + MR + + + + RF + RV + + + + HSP + AV + + + + P + P + + + + R + R + + + + LOC + LOC + + + + IC + IC + + + + EXP + EXP + + + + DIR + DIR + + + + JobChangeShiftDlg + + + Shift Error + Errore Turno + + + + Error while setting shift <b>%1</b> to job <b>%2</b>.<br>Msg: %3 + Errore nell'impostare il turno <b>%1</b> al servizio <b>%2</b>.<br>Msg: %3 + + + + Change shift for job <b>%1</b>: + Cambia il Turno del servizio <b>%1</b>: + + + + JobPassingsModel + + + Job + Servizio + + + + Arrival + Arrivo + + + + Departure + Partenza + + + + Platform + Binario + + + + JobPathEditor + + + Save Sheet + Salva Foglio + + + + Toggle transit + Commuta transito + + + + Remove + Rimuovi + + + + Insert before + Inserisci prima + + + + Edit stop + Modifica fermata + + + + Invalid Operation + Operazione Non Valida + + + + Transit cannot have coupling or uncoupling operations. +Remove theese operation to set Transit on this stop. + Nei transiti non si possono eseguire operazioni di Aggancio o Sgancio. +Rimuovere queste operazioni per trasformare la fermata in Transito. + + + + Set transit + Imposta transito + + + + Unset transit + Togli transito + + + + Error + Errore + + + + You must register at least 2 stops. +Do you want to delete this job? + Devi registrare almeno 2 fermate. +Vuoi eliminare questo servizio? + + + + Save? + Salvare? + + + + Do you want to save changes to job %1 + Vuoi salvare le modifiche fatte al servizio %1 + + + + Invalid + Non valido + + + + Job number <b>%1</b> is already exists.<br>Please choose a different number. + Il servizio numero <b>%1</b> è già esistente.<br>Per favore scegli un numero diverso. + + + + Empty Job + Servizio vuoto + + + + Before setting a shift you should add stops to this job + Prima di selezionare un turno dovresti aggiungere le fermate + + + + Save Job Sheet + Salva Foglio Servizio + + + + job%1_sheet.odt + foglio_treno%1.odt + + + + JobsManager + + + New Job + Nuovo Treno + + + + Remove + Rimuovi + + + + Remove All + Rimuovi Tutti + + + + Job deletion + Rimozione Trano + + + + Are you sure to delete job %1? + Sei sicuro di voler eliminare il trno %1? + + + + Delete all jobs? + Rimuovere tutti i servizi? + + + + Are you really sure you want to delete all jobs from this session? + Sei veramente sicuro di voler rimuovere tutti i servizi da questa sessione? + + + + JobsSQLModel + + + Number + Numero + + + + Category + Categoria + + + + Shift + Turno + + + + Origin + Stazione di Partenza + + + + Departure + Partenza + + + + Destination + Destinazione + + + + Arrival + Arrivo + + + + LinesSQLModel + + + Name + Nome + + + + Max. Speed + Velocità Max + + + + Type + Tipo + + + + %1 km/h + + + + + LoadTaskUtils + + + %1 +Code: %2 +Message: %3 + + + + + Could not open session file correctly. + + + + + Query preparation failed A1. + + + + + Query preparation failed A2. + + + + + Query preparation failed B1. + + + + + Query preparation failed B2. + + + + + + Query preparation failed C1. + + + + + + Query preparation failed C2. + + + + + MainWindow + + + File + + + + + Edit + Modifica + + + + New Job + Nuovo Treno + + + + Remove Job + Rimuovi Treno + + + + Rollingstock + Rotabili + + + + Save Copy As + Salva una copia + + + + Save a backup copy of the current session + Salva una copia di backup della sessione corrente + + + + Settings + Opzioni + + + + Help + Aiuto + + + + View + Visualizza + + + + Open + Apri + + + + Save + Salva + + + + Exit + Esci + + + + Add train job + Aggiungi servizio (Treno) + + + + Remove selected train job + Rimuovi servizio selezionato + + + + Stations + Stazioni + + + + New + Nuovo + + + + Close + Chiudi + + + + + About + Informazioni su + + + + About Qt + Informazioni su Qt + + + + Print + Stampa + + + + Export PDF + Esporta PDF + + + + Open Recent + Apri recenti + + + + Query + + + + + Session Rollingstock Summary + Riassunto rotabili sessione + + + + Job Shifts + Turni servizi + + + + Show rollingstock position at start or end of session + Mostra la posizione dei rotabili all'inizio o fine della sessione + + + + Meeting Information + Informazioni meeting + + + + Export Svg + Esporta Svg + + + + Jobs + Servizi + + + + Next Job Segment + Segmento Succesivo del Servizio + + + + Prev Job Segment + Segmento Precedente del Servizio + + + + Properties + Proprietà + + + + <p>Open a file: <b>File</b> > <b>Open</b></p><p>Create new project: <b>File</b> > <b>New</b></p> + <p>Apri un file: <b>File</b> > <b>Apri</b></p><p>Crea un nuovo progetto: <b>File</b> > <b>Nuovo</b></p> + + + + Find + Trova + + + + Open file or create a new one + Apri un file o creane uno nuovo + + + + + Backgroung Task + Task in background + + + + Error saving copy + Errore salvataggio copia + + + + There was an error while closing the database. +Make sure there aren't any background tasks running and try again. + C'è stao un problema nella chiusura del database. +Assicurarsi che tutte le operazioni in background siano terminate e riprovare. + + + + &%1 %2 + + + + + About %1 + Informazioni su %1 + + + + Choose line + Seleziona linea + + + + Choose a railway line + Selezionare una linea ferroviaria + + + + %1 application makes easier to deal with timetables and trains +Beta version: %2 +Built: %3 + L'applicazione %1 facilita la gestione di orari e treni +Versione Beta: %2 +Built: %3 + + + + + Background task for checking rollingstock errors is still running. +Do you want to cancel it? + Il task in background per il controllo errori dei rotabili è ancora in esecuzione. +Vuoi annullarlo? + + + + Open Session + Apri sessione + + + + Version is old + Vecchia versione + + + + This file was created by an older version of Train Timetable. +Opening it without conversion might not work and even crash the application. +Do you want to open it anyway? + Questo file è stato creato da una vecchia versione di Train Timetable. +La sua apertura senza conversione può causare malfunzionamenti e crashare l'applicazione. +Vuoi aprirlo lo stesso? + + + + Version is too new + Versione troppo recente + + + + This file was created by a newer version of Train Timetable. +You should update the application first. Opening this file might not work or even crash. +Do you want to open it anyway? + Questo file è stato creato da una versione più recente di Train Timetable. +Dovresti prima aggiornare l'applicazione. L'apertura di questo file potrebbe non funzionare o crashare l'applicazione. +Vuoi aprirlo comunque? + + + + RS Import + Importa Rotabili + + + + There is some rollingstock import data left in this file. Probably the application has crashed!<br>Before deleting it would you like to resume importation?<br><i>(Sorry for the crash, would you like to contact me and share information about it?)</i> + In questo file sono rimasti dei dati relativi all'importazione rotabili. Probabilmente l'applicazione è crashata!<br> +Prima di eliminarli, vuoi riprendere l'importazione?<br><i>(Mi dispiace per il crash, vorresti contattarmi per condividere i dettagli del malfunzionamento?)</i> + + + + Resume importation + Riprendi importazione + + + + Just delete it + Elimina e basta + + + + + Background Tasks + Task in background + + + + Some background tasks are still running. +The file was not opened. Try again. + Alcune operazioni in background devono ancora erminare. +Il file non è stato aperto. Riprova. + + + + Create new Session + Crea nuova Sessione + + + + Some background tasks are still running. +The new file was not created. Try again. + Alcune operazioni in background devono ancora erminare. +Il file non è stato creato. Riprova. + + + + Save Session Copy + Salva Copia Sessione + + + + Error while Closing + Errore nella chiusura + + + + <p><b>There are no lines in this session</b></p><p><table align="center"><tr><td>Start by creating the railway layout for this session:</td></tr><tr><td><table><tr><td>1.</td><td>Create stations (<b>Edit</b> > <b>Stations</b>)</td></tr><tr><td>2.</td><td>Create railway lines (<b>Edit</b> > <b>Stations</b> > <b>Lines Tab</b>)</td></tr><tr><td>3.</td><td>Add stations to railway lines</td></tr><tr><td></td><td>(<b>Edit</b> > <b>Stations</b> > <b>Lines Tab</b> > <b>Edit Line</b>)</td></tr></table></td></tr></table></p> + <p><b>Non ci sono linee in questa sessione</b></p><p><table align="center"><tr><td>Inizia creando il layout delle linee ferroviarie per questa sessione:</td></tr><tr><td><table><tr><td>1.</td><td>Crea le stazioni (<b>Modifica</b> > <b>Stazioni</b>)</td></tr><tr><td>2.</td><td>Crea le linee ferroviarie (<b>Modifica</b> > <b>Stazioni</b> > <b>Pagina Linee</b>)</td></tr><tr><td>3.</td><td>Aggiungi le stazioni alle linee ferroviarie</td></tr><tr><td></td><td>(<b>Modifica</b> > <b>Stazioni</b> > <b>Pagina Linee</b> > <b>Modifica Linea</b>)</td></tr></table></td></tr></table></p> + + + + You must create at least one line before adding job to this session + Devi creare almeno una linea prima di aggiungere un servizio a questa sessione + + + + MeetingInformationDialog + + + Meeting Information + Informazioni meeting + + + + Meeting + Sessione + + + + Meeting start day: + Data inizio sessione: + + + + Meeting end day: + Data termine sessione: + + + + City: + Città: + + + + Hosting association: + Associazione ospitante: + + + + Picture Logo + Immagine logo + + + + View picture + Vedi immagine + + + + Import Picture + Importa immagine + + + + Remove Picture + Rimuovi immagine + + + + Sheet export + Esportazione Fogli + + + + Footer text: + Testo piè pagina: + + + + Header text: + Testo intestazione: + + + + + + Reset + Resetta + + + + Show meeting dates on first page + Mostra le date del meeting nella prima pagina + + + + Meeting description + Descrizione Meeting + + + + Database Error + Errore database + + + + This database doesn't support metadata. +Make sure it was created by a recent version of the application and was not manipulated. + Questo database non supporta i metadati. +Assicurarsi che sia stato creato da una versione recente dell'applicazione e che non sia stato manipolato. + + + + Set custom text + Imposta testo personalizzato + + + + Import image + Importa immagine + + + + Importing error + Errore importazione + + + + The image format is not supported or the file is corrupted. + L'immagine non è supportata oppure il file è corrotto. + + + + Remove image? + Rimuovere immagine? + + + + Are you sure to remove the image logo? + Sei sicuo di voler rimuovere l'immagine di logo? + + + + MergeModelsDialog + + + Merge Models + Unisci Modelli + + + + Merge model A in B: all RS of model A will be changed to model B + Unisci modello A con il modello B: tutti i rotabili di modello A verranno cambiati in modello B + + + + + Invalid + + + + + Remove model A + Rimuovi modello A + + + + Invalid Models + Modelli non validi + + + + Models must not be null and must be different + I moelli devono essere validi e differenti + + + + Models merged succesfully. + Modelli uniti con successo. + + + + Error while merging + Errore + + + + Some error occurred while merging models. + Errore nell'unione modelli. + + + + No model set + Modello non impostato + + + + Error + Errore + + + + %1Axes: <b>%2</b> Max.Speed: <b>%3 km/h</b><br>Type: <b>%4</b> + %1Assi: <b>%2</b> Vel.Max: <b>%3 km/h</b><br>Tipo: <b>%4</b> + + + + Merging completed + Unione completata + + + + MergeOwnersDialog + + + Merge Owners + Unisci Proprietari + + + + Merge owner A in B: all RS of owner A will be changed to owner B + Unisci proprietario A con B: tutti i rotabili del proprietario A diventano del proprietario B + + + + Remove Owner A + Rimuovi proprietario A + + + + Invalid Owners + Proprietari non validi + + + + Owners must not be null and must be different + I proprietadi devono essere validi e differenti + + + + Owners merged succesfully. + Proprietari uniti con successo. + + + + Error while merging + Errore + + + + Some error occurred while merging owners. + Errore nell'unione proprietari. + + + + Merging completed + Unione completata + + + + ModelPageSwitcher + + + Prev + Precedente + + + + Next + Successivo + + + + Go + Vai + + + + Refresh + Ricarica + + + + Page %1/%2 (%3 items) + Pagina %1/%2 (%3 elementi) + + + + ODSOptionsWidget + + + Import rollingstock pieces, models and owners from a spreadsheet file. +The file must be a valid Open Document Format Spreadsheet V1.2 +Extension: (*.ods) + Importa rotabili, modelli e proprietari da un foglio di calcolo. +Il file deve essere di formato Open Document Format Spreadsheet V1.2 valido +Estensione: (*.ods) + + + + First non-empty row that contains rollingstock piece information + Prima riga non vuota che contiene le informazioni di un rotabile + + + + Column from which item number is extracted + Colonna da cui è estratto il numero + + + + Column from which item model name is extracted + Colonna da cui è estratto il nome modello + + + + Odt + + + From: + Da: + + + + Departure: + Partenza: + + + + To: + Per: + + + + Arrival: + Arrivo: + + + + Axes: + Assi: + + + + + + Station + Stazione + + + + + + Arrival + Arrivo + + + + + + Departure + Partenza + + + + + + Platf + Bin + + + + + + Rollingstock + Rotabili + + + + + Crossings + Incroci + + + + + Passings + Precedenze + + + + + Notes + Note + + + + Transit + Transito + + + + Station: %1%2 + Stazione %1%2 + + + + (%1) + (%1) + + + + Job N + Treno N + + + + From + Da + + + + To + Per + + + + Shift %1 + Turno %1 + + + + Page + Pagina + + + + Job + Servizio + + + + + Owner + Proprietario + + + + Rollingstock by %1 at %2 of session + Rotabili per %1 a %2 sessione + + + + start + inizio + + + + end + fine + + + + From %1 to %2 + Dal %1 al %2 + + + + SHIFT %1 + TURNO %1 + + + + Meeting in %1 from %2 to %3 + Meeting a %1 dal %2 al %3 + + + + Meeting in %1 on %2 + Meeting a %1 il %2 + + + + Meeting + Sessione + + + + %1 station + Stazione di %1 + + + + Cp: + Ag: + + + + Unc: + Sg: + + + + OptionsPage + + + Default speed is applied when a rollingstock model is not matched to an existing one and has to be created from scratch + La velocità di default viene applicata quando un modello non è associato ad uno esistente e verrà creato da zero + + + + Default speed + Velocità di default + + + + Default type is applied when a rollingstock model is not matched to an existing one and has to be created from scratch + Il tipo di default viene applicato quando un modello non è associato ad uno esistente e verrà creato da zero + + + + Default type + Tipo di default + + + + PrintWizard + + + Print Wizard + Wizard di stampa + + + + PrinterOptionsPage + + + Open settings + Apri preferenze + + + + Printer + Stampante + + + + Printer options + Opzioni Stampante + + + + ProgressPage + + + Printing... + Sto stampando... + + + + Printing + Stampa in corso + + + + Completed + Completato + + + + PropertiesDialog + + + File Properties + Proprietà File + + + + File Path: + Percorso File: + + + + No opened file + Nessun file aperto + + + + RSCoupleDialog + + + Engines + Locomotive + + + + Coaches + Carozze + + + + Freight Wagons + Carri Merci + + + + <p style="font-size:13pt"><span style="background-color:#FF56FF">___</span> The item isn't coupled before or already coupled.<br><span style="background-color:#FF3d43">___</span> The item isn't in this station.<br><span style="color:#0000FF;background-color:#FFFFFF">\\\\</span> Railway line doesn't allow electric traction.<br><span style="background-color:#00FFFF">___</span> First use of this item.<br><span style="background-color:#00FF00">___</span> This item is never used in this session.</p> + <p style="font-size:13pt"><span style="background-color:#FF56FF">___</span> Questo elemento non è stato agganciato precedentemente o è già agganciato.<br><span style="background-color:#FF3d43">___</span> Questo elemento non è in questa stazione.<br><span style="color:#0000FF;background-color:#FFFFFF">\\\\</span> La linea ferroviaria non consente la trazione elettrica.<br><span style="background-color:#00FFFF">___</span> Primo utilizzo di questo elemento.<br><span style="background-color:#00FF00">___</span> Questo elemento non viene mai utilizzato in questa sessione.</p> + + + + Hide legend + Nascondi legenda + + + + Show legend + Mostra legenda + + + + RSCouplingInterface + + + + Error + Errore + + + + Error while adding coupling operation. +Rollingstock %1 is already coupled by this job (%2) + Errore aggiungendo un'operazione di aggancio. +Il rotabile %1 è già agganciato a questo treno (%2) + + + + Error while adding coupling operation. +Rollingstock %1 is already coupled to another job (%2) +Do you still want to couple it? + Errore aggiungendo un'operazione di aggancio. +Il rotabile %1 è già agganciato ad un altro treno (%2) +Vuoi agganciarlo lo stesso? + + + + Warning + Attenzione + + + + Rollingstock %1 is an Electric engine but the line is not electrified +This engine will not be albe to move a train. +Do you still want to couple it? + Il rotabile %1 è una locomotiva Elettrica ma la linea non è elettrificata +Questa locomotiva non potrà trainare un treno. +Vuoi comunque agganciarla? + + + + Delete coupling? + Eliminare operazione di aggancio? + + + + You couple %1 also in a next stop in %2 at %3. +Do you want to remove the other coupling operation? + Il rotabile %1 viene agganciato anche nella successiva fermata a %2 alle %3. +Vuoi eliminare l'altra operazione di aggancio? + + + + + Delete uncoupling? + Eliminare operazione di sgancio? + + + + You don't couple %1 anymore. +Do you want to remove also the uncoupling operation in %2 at %3? + Il rotabile %1 non viene più agganciato. +Vuoi rimuovere anche l'operazione di sgancio a %2 alle %3? + + + + You uncouple %1 also in %2 at %3. +Do you want to remove the other uncoupling operation? + Il rotabile %1 viene sganciato anche a %2 alle %3. +Vuoi rimuovere l'altra operazione di sgancio? + + + + RSImportWizard + + + Completed. + Completato. + + + + RSImportedModelsModel + + + Models with empty name must have a Custom Name or must be matched to an existing model + I modelli con nome vuoto devono avere un Nome Personalizzato o devono essere mappati ad un modello esistente + + + + You cannot set the same name in the 'Custom Name' field. +If you meant to revert to original name then clear the custom name and leave the cell empty + Non puoi impostare lo stesso nome nel campo Nome Personalizzato. +Se intendevi riportarlo al nome originale allora lascia il campo vuoto + + + + There is already an existing Model with same name: <b>%1</b> +If you meant to merge theese rollingstock pieces with this existing model please use 'Match Existing' field + Esiste già un modello con lo stesso nome: <b>%1</b><br>Se intendevi unire questi rotabili con un modello esistente, per favore usa il campo 'Mappa a esistente' + + + + There is already an imported Model with name: <b>%1</b> +If you meant to merge theese rollingstock pieces with this existing model after importing rollingstock use the merge tool to merge them + Esiste già un modello importato con il nome: <b>%1</b><br>Se intendevi unire questi rotabili con quest'altro modello, terminata l'importazione usa lo strumento Unisci Modelli + + + + You already gave the same custom name: <b>%1</b> to the imported model: <b>%2</b> +In order to proceed you need to assign a different custom name to %2 + Hai già assegnato lo stesso nome personalizzato: <b>%1</b> al modello importato: <b>%2</b><br>Per procedere devi assegnare un nome differente a %2 + + + + You already gave the same custom name: <b>%1</b> to the imported model: <b>%2</b> +Please choose a different name or leave empty for the original name + Hai già assegnato lo stesso nome personalizzato: <b>%1</b> al modello importato: <b>%2</b><br>Per favore scegli un nome differente o lascia il campo vuoto per mantenere il nome originale + + + + RSImportedOwnersModel + + + Owners with empty name must have a Custom Name or must be matched to an existing owner + I proprietari con nome vuoto devono avere un Nome Personalizzato o devono essere mappati ad un proprietario esistente + + + + You cannot set the same name in the 'Custom Name' field. +If you meant to revert to original name then clear the custom name and leave the cell empty + Non puoi impostare lo stesso nome nel campo Nome Personalizzato. +Se intendevi riportarlo al nome originale allora lascia il campo vuoto + + + + There is already an existing Owner with same name: <b>%1</b> +If you meant to merge theese rollingstock pieces with this existing owner please use 'Match Existing' field + Esiste già un proprietario con lo stesso nome: <b>%1</b><br>Se intendevi unire questi rotabili con un proprietario esistente, per favore usa il campo 'Mappa a esistente' + + + + There is already an imported Owner with name: <b>%1</b> +If you meant to merge theese rollingstock pieces with this existing owner after importing rollingstock use the merge tool to merge them + Esiste già un proprietario importato con il nome: <b>%1</b><br>Se intendevi unire questi rotabili con quest'altro proprietario, terminata l'importazione usa lo strumento Unisci Proprietari + + + + You already gave the same custom name: <b>%1</b> to the imported owner: <b>%2</b> +In order to proceed you need to assign a different custom name to %2 + Hai già assegnato lo stesso nome personalizzato: <b>%1</b> al proprietario importato: <b>%2</b><br>Per procedere devi assegnare un nome differente a %2 + + + + You already gave the same custom name: <b>%1</b> to the imported owner: <b>%2</b> +Please choose a different name or leave empty for the original name + Hai già assegnato lo stesso nome personalizzato: <b>%1</b> al proprietario importato: <b>%2</b><br>Per favore scegli un nome differente o lascia il campo vuoto per mantenere il nome originale + + + + RSImportedRollingstockModel + + + You cannot set the same name in the 'Custom Name' field. +If you meant to revert to original name then clear the custom name and leave the cell empty + Non puoi impostare lo stesso nome nel campo Nome Personalizzato. +Se intendevi riportarlo al nome originale allora lascia il campo vuoto + + + + There is already another imported rollingstock with same 'New Number': <b>%1 %2</b> + Esiste già un altro rotabile importato con lo stesso campo 'Nuovo numero': <b>%1 %2</b> + + + + There is already another imported rollingstock with same number: <b>%1 %2</b> + Esiste già un altro rotabile importato con lo stesso numero: <b>%1 %2</b> + + + + There is already an existing rollingstock with same number: <b>%1 %2</b> + C'e già un rotabile esistente con lo stesso numero: <b>%1 %2</b> + + + + RSJobViewer + + + Update + Aggiorna + + + + Type: <b>%1</b><br>Owner: <b>%2</b> + Tipo: <b>%1</b><br>Proprietario: <b>%2</b> + + + + Not set! + No impostato! + + + + Show in Job Editor + Mostra nell'Editor Servizio + + + + RSModelsSQLModel + + + This model name (<b>%1</b>) is already used with the same suffix (<b>%2</b>). +If you intend to create a new model of same name but different suffix, please first set the suffix. + Questo nome modello (<b>%1</b>) è già in uso con lo stesso suffisso (<b>%2</b>).<0br>Se intendi creare un nuovo modello con lo stesso nome ma suffusso differente, per favore imposta prima il suffisso. + + + + This model suffix (<b>%1</b>) is already used with the same name (<b>%2</b>). + Questo suffisso del modello (<b>%1</b>) è già in uso con lo stesso nome (<b>%2</b>). + + + + Rollingstock maximum speed must be > 0 km/h. + La velocità massima di un rotabile deve essere > 0 km/h. + + + + Rollingstock must have at least 2 axes. + I rotabile devono avere almeno 2 assi. + + + + There are rollingstock pieces of model <b>%1</b> so it cannot be removed. +If you wish to remove it, please first delete all <b>%1</b> pieces. + Ci sono rotabili di modello <b>%1</b> perciò non può essere rimosso.<br>Se desideri rimuoverlo, per favore elimina prima tutti i rotabili <b>%1</b>. + + + + Suffix is already used. Suffix must be different among models of same name. + Questo suffisso è già in uso. I suffissi devono essere diversi per modelli con lo stesso nome. + + + + RSOwnersSQLModel + + + This owner name (<b>%1</b>) is already used. + Questo nome proprietario (<b>%1</b>) è già in uso. + + + + There are rollingstock pieces of owner <b>%1</b> so it cannot be removed. +If you wish to remove it, please first delete all <b>%1</b> pieces. + Ci sono rotabili con proprietario <b>%1</b> perciò non può essere rimosso.<br>Se desideri rimuoverlo, per favore elimina prima tutti i rotabili di <b>%1</b>. + + + + RSProxyModel + + + Rollingstock <b>%1</b> cannot be uncoupled here because it wasn't coupled to this job before this stop or because it was already uncoupled before this stop.<br>Please remove the tick + Il rotabile <b>%1</b> non può essere sganciato qui perchè non era stato precedentemente agganciato a questo servizio o perchè viene già sganciato prima di questa fermata.<br>Per favore rimuovere la spunta + + + + Rollingstock <b>%1</b> cannot be coupled here because it was already coupled to this job before this stop<br>Please remove the tick + Il rotabilek <b>%1</b> non può essere agganciato qui perchè è già stato agganciato a questo treno prima di questa fermata.<br>Perfavore togli la spunta + + + + Rollingstock <b>%1</b> cannot be coupled here because it was already coupled before this stop<br>to job <b>%2<b/><br>Please remove the tick + Il rotabile <b>%1</b> non può essere agganciato qui perchè è già stato agganciato prima di questa fermata<br>al servizio <b>%2<b/><br>Perfavore togli la spunta + + + + Rollingstock <b>%1</b> cannot be coupled here because it is not in this station.<br>Please remove the tick + Il rotabile <b>%1</b> non può essere agganciato qui perchè non è in questa stazione.<br>Per cortesia rimuovere la spunta + + + + Engine <b>%1</b> is electric but the line is not electrified! + La locomotiva <b>%1</b> è a trazione elettrica ma la linea non è elettrificata! + + + + Rollingstock <b>%1</b> is coupled in this station also by <b>%2</b> at <b>%3</b>. + Il rotabile <b>%1</b> è agganciato in questa stazione anche dal servizio <b>%2</b> alle <b>%3></b>. + + + + Rollingstock <b>%1</b> was left in this station by <b>%2</b> at <b>%3</b>. + Il rotabile <b>%1</b> viene lasciato in questa stazione dal servizio <b>%2</b> alle <b>%3></b>. + + + + This is the first use of this rollingstock <b>%1</b> + Questo è il primo utilizzo del rotabile <b>%1</b> + + + + This would be the first use of this rollingstock <b>%1</b> + Qesto sarebbe il primo utilizzo del rotabile <b>%1</b> + + + + Rollingstock <b>%1</b> is never used in this session. You can couple it for the first time from any one station + Il rotabile <b>%1</b> non viene mai utilizzato in questa sessione. Puoi agganciarlo la prima volta in una qualsiasi stazione + + + + RailwayNodeEditor + + + Add + Aggiungi + + + + Remove + Rimuovi + + + + Please choose a line for the new entry + Per favore scegli una linea per il novo elemento + + + + Line + Linea + + + + Please choose a station for the new entry + Per favore scegli una stazione per il novo elemento + + + + Station + Stazione + + + + You must select a valid line. + Devi selezionare una linea valida. + + + + You must select a valid station. + Devi selezionare una stazione valida. + + + + You cannot add the same line twice to the same station. + Non puoi aggiungere la stessa linea due volte alla stessa stazione. + + + + You cannot add the same station twice to the same line. + Non puoi aggiungere la stessa stazione due volte alla stessa linea. + + + + RailwayNodeModel + + + Rollingstock item <b>%1</b> is used in some jobs so it cannot be removed. +If you wish to remove it, please first remove it from its jobs. + Il rotabile <b>%1</b> è utilizzato in alcuni servizi perciò non può essere rimosso.<br>Se desideri rimuoverlo, prima rimuovilo dai suoi servizi. + + + + Line + Linea + + + + Station + Stazione + + + + Km + + + + + Direction + Direzione + + + + RollingStockManager + + + Rollingstock Manager + Manager Rotabili + + + + New Rollingstock + Nuovo Rotabile + + + + + + Remove + Rimuovi + + + + View Plan + Vedi Piano + + + + Search rollingstock item + Cerca rotabile + + + + Delete All Rollingstock + Rimuovi tutti i Rotabili + + + + Import + Importa + + + + Session Summary + Riassunto della Sessione + + + + New Model + Nuovo Modello + + + + Merge Models + Unisci Modelli + + + + New with suffix + Nuovo con suffisso + + + + Search rollingstock model + Cerca modello rotabile + + + + Delete All Models + Rimuovere tutti i modelli + + + + New Owner + Nuovo Proprietario + + + + Merge Owners + Unisci Proprietari + + + + Delete All Owners + Rimuovere tutti i proprietari + + + + Please choose a rollingstock item + Per favore scegliere un rotabile + + + + [model][.][number][:owner] + [modello][.][numero][:proprietario] + + + + Error adding rollingstock piece + Errore nell'aggiungere un rotabile + + + + Delete All Rollingstock? + Rimuovi tutti i Rotabili? + + + + Are you really sure you want to delete all rollingstock from this session? +NOTE: this will not erease model and owners, just rollingstock pieces. + Sei veramente sicuro di voler rimuovere tutti i rotabili da questa sessione? +NOTA: i modelli e i proprietari non verranno rimossi, solo i rotabili. + + + + Failed to remove rollingstock. +Make sure there are no more couplings in this session. +NOTE: you can remove all jobs at once from the Jobs Manager. + Impossibile rimuovere i rotabili. +Assicurarsi che non ci siano operazioni di aggancio/sgancio in questa sessione. +NOTE: puoi rimuovere tutti i servizi in un colpo solo dal Manager Servizi. + + + + Error adding model + Errore nell'aggiungere un modello rotabile + + + + Delete All Rollingstock Models? + Rimuovi tutti i Modelli di Rotabili? + + + + Are you really sure you want to delete all rollingstock models from this session? +NOTE: this can be done only if there are no rollingstock pieces in this session. + Sei veramente sicuro di voler rimuovere tutti i modelli di rotabili da questa sessione? +NOTA: questo può essere fatto solo se non ci sono più rotabili in questa sessione. + + + + Failed to remove rollingstock models. +Make sure there are no more rollingstock pieces in this session. +NOTE: you can remove all rollinstock pieces at once from the Rollingstock tab. + Impossibile rimuovere i modelli di rotabili. +Assicurarsi che non ci siano più rotabili in questa sessione. +NOTE: puoi rimuovere tutti i rotabili in un colpo solo dal Manager Rotabili. + + + + Delete All Rollingstock Owners? + Rimuovi tutti i Proprietari di Rotabili? + + + + Are you really sure you want to delete all rollingstock owners from this session? +NOTE: this can be done only if there are no rollingstock pieces in this session. + Sei veramente sicuro di voler rimuovere tutti i proprietari di rotabili da questa sessione? +NOTA: questo può essere fatto solo se non ci sono più rotabili in questa sessione. + + + + Failed to remove rollingstock owners. +Make sure there are no more rollingstock pieces in this session. +NOTE: you can remove all rollingstock pieces at once from the Rollingstock tab. + Impossibile rimuovere i proprietari di rotabili. +Assicurarsi che non ci siano più rotabili in questa sessione. +NOTE: puoi rimuovere tutti i rotabili in un colpo solo dal Manager Rotabili. + + + + + + + Error + Errore + + + + Please choose a rollingstock model + Per favore scegli un modello rotabile + + + + Model + Modello + + + + You must select a valid rollingstock model. + Devi selezionare un modello rotabile valido. + + + + Please choose an unique suffix for this model, or leave empty + Per favore scegli un suffisso unico per questo modello o lascia vuoto + + + + Choose Suffix + Scegli Suffisso + + + + RollingstockSQLModel + + + Rollingstock item <b>%1</b> is used in some jobs so it cannot be removed.<br>If you wish to remove it, please first remove it from its jobs. + Il rotabile <b>%1</b> è utilizzato in alcuni treni quindi non può essere eliminato.<br>Se vuoi eliminarlo, rimuovilo prima dai suoi servizi. + + + + RsErrorTreeModel + + + Job + Servizio + + + + Station + Stazione + + + + Arrival + Arrivo + + + + Description + Descrizione + + + + RsErrors + + + Stop is transit. Cannot couple/uncouple rollingstock. + La fermata è un transito. Non è possibile agganciare/sganciare rotabili. + + + + Coupled while busy: it was already coupled to another job. + Agganciato mentre è impegnato: è già agganciato ad un altro servizio. + + + + Uncoupled when not coupled. + Sganciato quando non era agganciato. + + + + Not uncoupled at the end of the job or coupled by another job before this jobs uncouples it. + Non viene sganciato al termine del servizio oppure un altro servizio lo aggancia prima che venga sganciato da questo servizio. + + + + Coupled in a different station than that where it was uncoupled. + Agganciato in una stazione differente da quella in cui era stato sganciato. + + + + Uncoupled in the same stop it was coupled. + Sganciato nella stessa fermata in cui viene agganciato. + + + + RsErrorsWidget + + + Start + Avvia + + + + Stop + Ferma + + + + Rollingstock Errors + Errori Rotabili + + + + Show in Job Editor + Mostra nell'Editor Servizio + + + + Show rollingstock plan + Mostra il piano giornaliero del rotabile + + + + RsImportStrings + + + + + + + + Import + Importa + + + + + Sheet No. + Foglio N. + + + + + + Name + Nome + + + + + + Custom Name + Nome personalizzato + + + + + Model + Modello + + + + + Number + Numero + + + + + New number + Nuovo numero + + + + + Owner + Proprietario + + + + + Match Existing + Mappa a esistente + + + + Insert path here or click 'Choose' button + Inserire percorso qui o cliccare il bottone 'Scegli' + + + + Choose + Scegli + + + + Choose file + Scegli file + + + + Choose a file to import in *.ods format + Scegli file in formato *.ods da iportare + + + + File doesn't exist + Il file non esiste + + + + Could not find file '%1' + Impossibile trovare file '%1' + + + + Open Spreadsheet + Apri foglio di calcolo + + + + Open Session + Apri sessione + + + + The file constains some duplicates in item names wich need to be fixed in order to procced. +There also may be some items with empty name. +Please assign a custom name to them so that there are no duplicates + Il file contiene elementi con nomi duplicati che vanno corretti prima di procedere. +Potrebbero anche esserci proprietarie con nome vuoto. +Per favore assegna un nome personalizzato in maniera tale da non avere duplicati + + + + Fix Item Names + Correggi nomi elementi + + + + + + Invalid Operation + Operazione Non Valida + + + + Not yet! + Non ancora! + + + + There are still %1 items to be fixed + Ci sono ancora %1 elementi da correggere + + + + + Previuos page + Pagina precedente + + + + Aborting RS Import + Annullamento importazione rotabili + + + + If you don't fix duplicated items you cannot proceed. +Do you wish to Abort the process? + Se non correggi gli elementi duplicati non puoi procedere. +Vuoi annullare il processo di importazione? + + + + Select owners of rollingstock you want to import + Seleziona i proprietari che vuoi importare + + + + Select models of rollingstock you want to import + Seleziona i modelli che vuoi importare + + + + Select rollingstock pieces you want to import + Seleziona i rotabili che vuoi importare + + + + You must select at least 1 owner + Devi selezionare almeno 1 proprietario + + + + You must select at least 1 model + Devi selezionare almeno 1 modello + + + + You must select at least 1 rollingsock piece + Devi selezionare almeno 1 rotabile + + + + %1 owners selected + %1 proprietari selezionati + + + + %1 models selected + %1 modelli selezionati + + + + %1 rollingstock pieces selected + %1 rotabili selezionati + + + + General options + Opzioni generail + + + + Import rollingstick owners + Importa proprietari rotabili + + + + Import rollingstick models + Importa modelli rotabili + + + + Import rollingstick pieces + Importa rotabili + + + + Import options + Opzioni importazione + + + + Import source: + Sorgente importazione: + + + + Invalid option + Opzione non valida + + + + You must at least import owners or models + Devi almeno importare Proprietari o Modelli + + + + No rolloingstock imported + Nessun rotabile importato + + + + No rollingstock piece will be imported. +In order to import rollingstock pieces you must also import models and owners. + Nessun rotabile verrà importato. +Per importare i rotabili devi importare anche Modelli anche Proprietari. + + + + In order to import rollingstock pieces you must also import models and owners. + Per importare i rotabili devi importare anche Modelli anche Proprietari. + + + + File loading + Caricamento file + + + + Parsing file data... + Lettura dati file... + + + + Importing + Importazione + + + + Importing data... + Importazione dati... + + + + Original + Originale + + + + Abort import? + Annullare importazione? + + + + Do you want to import process? No data will be imported + Vuoi annullare l'importazione? Nessun dato sarà importato + + + + + Aborting... + Annullamento... + + + + Loading Error + Errore di caricamento + + + + Error + Errore + + + + Invalid option selected. Please try again. + Operazione non valida selezionata. Per favore riprovare. + + + + RsPlanModel + + + Job + Servizio + + + + Station + Stazione + + + + Arrival + Arrivo + + + + Departure + Partenza + + + + Operation + Operazione + + + + Coupled + Agganciato + + + + Uncoupled + Sganciato + + + + RsTypeNames + + + Engine + Locomotiva + + + + Freight Wagon + Carro Merci + + + + Coach + Carrozza + + + + Not set! + No impostato! + + + + Electric + Elettrica + + + + Diesel + Diesel + + + + Steam + Vapore + + + + + + Suffix + Suffisso + + + + + Max Speed + Velocità Max + + + + Axes + Assi + + + + + + Type + Tipo + + + + Sub Type + Sottotipo + + + + + Owners + Proprietari + + + + + Models + Modelli + + + + + Rollingstock + Rotabili + + + + Model + Modello + + + + Number + Numero + + + + Owner + Proprietario + + + + + Name + Nome + + + + N. Axes + N. Assi + + + + Subtype + Sottotipo + + + + SQLViewer + + + Query Error + + + + + SQLite Error: %1 +Extended: %2 +Message: %3 + + + + + Preparation Failed + + + + + SQLiteOptionsWidget + + + Import rollingstock pieces, models and owners from another Train Timetable session file. +The file must be a valid Train Timetable session of recent version +Extension: (*.ttt) + Importa rotabili, modelli e proprietari da un'altra sessione Train Timetable. +Il file deve essere di formato Train Timetable Session valido e recente +Estensione: (*.ods) + + + + SelectionPage + + + Select All + Seleziona Tutto + + + + Unselect All + Deseleziona Tutto + + + + Selection page + Pagina di selezione + + + + Select one or more lines to be printed + Selezionare una o più linee da stampare + + + + SessionStartEndModel + + + Rollingstock + Rotabili + + + + Job + Servizio + + + + Platform + Binario + + + + Departure + Partenza + + + + Arrival + Arrivo + + + + Owner + Proprietario + + + + Station + Stazione + + + + SessionStartEndRSViewer + + + Show Session Start + Mostra Inizio Sessione + + + + Show Session End + Mostra Fine Sessione + + + + Order By Station + Ordina per Stazione + + + + Order By Owner + Ordina per Proprietario + + + + Export Sheet + Esporta Foglio + + + + Rollingstock Summary + Riassunto Rotabili + + + + Expoert RS session plan + Esporta il piano rotabili della sessione + + + + SettingsDialog + + + General + Generale + + + + Language + Lingua + + + + Language changes will be applied next time you start the application + Le impostazioni della lingua verranno applicate al prossimo avvio dell'applicazione + + + + Restore Default Settings + Ripristina Impostazioni Predefinite + + + + Job Graph + Grafico Servizi + + + + + Offsets + Distanze + + + + Stations + Stazioni + + + + + Hours + Ore + + + + + Platforms + Binari + + + + + Horizontal + Bordo Orizzontale + + + + + Vertical + Bordo Verticale + + + + Hour Line Start + Inizio linea ore + + + + Line Width + Spessore Linea + + + + + Job + Servizio + + + + Hour + Ora + + + + Colors + Colori + + + + Hour Line + Linea Ore + + + + Hour Text + Testo Ora + + + + Main Platform + Binario Principale + + + + Depot Platform + Binario Deposito + + + + Station Text + Testo Stazioni + + + + Auto insert transits between two stops. + Inserisci automaticamente i transiti tra due fermate. + + + + Open Document Spreadsheet Import + Importa Foglio di calcolo OpenDocument + + + + First non-empty row + Prima riga non vuota + + + + Number column + Colonna numeri + + + + Model column + Colonna modelli + + + + Sheet Export + Esportazione Fogli + + + + Description + Descrizione + + + + Header + Intestazione + + + + Footer + Piè pagina + + + + Metadata + Metadati + + + + Store meeting location and dates in sheet metadata + Salva città e date del meeting nei metadati del foglio + + + + Background Tasks + Task in background + + + + Rollingstock Error Checker + Controllo errori Rotabili + + + + Check rollingstock when opening a file + Controlla rotabili quando viene aperto un file + + + + Check rollingstock when a Job is edeited + Controlla rotabili quando viene modificato un servizio + + + + Job Colors + Colori Servizi + + + + Stop + Fermata + + + + Default Stop Duration + Durata Fermata Predefinita + + + + Job Editor + Editor Servizi + + + + Choose next line before editing newly added last stop. + Scegli linea prima di scegliere l'ultima fermata aggiunta. + + + + Auto uncouple all rollingstock items at last stop. + Sgancia automaticamente tutti i rotabili nell'ultima fermata. + + + + Auto move uncoupled rollingstock pieces from last stop when adding a new one. + Sposta automaticamente i rotabili sganciati dall'ultima fermata quando ne viene aggiunta una nuova. + + + + Shift Graph + Grafico Turni + + + + Job Box + + + + + Stations Labels + Etichette stazioni + + + + Hide Same Stations + Nascondi stesse stazioni + + + + Rollingstock + Rotabili + + + + Merge models + Unisci Modelli + + + + Remove source merged model by default + Rimuovi di default il proprietario d'origine + + + + Merge owners + Unisci Proprietari + + + + Remove sourcemerged owner by default + Rimuovi di default il modello d'origine + + + + + + Settings + Impostazioni + + + + minutes + minuti + + + + Non Passenger + Non Passeggeri + + + + Passenger + Passeggeri + + + + Do you want to save settings? + Vuoi salvare le impostazioni? + + + + Do you want to restore default settings? + Vuoi ripristinare le impostazioni predefinite? + + + + ShiftBusyDlg + + + Shift is busy + Turno Occupato + + + + Cannot set shift <b>%1</b> to job <b>%2</b>.<br>The selected shift is busy:<br>From: %3 To: %4 + Impossibile impostare il turno <b>%1</b> per il servizio <b>%2</b>.<br>Il turno selezionato è impagnato:<br>Dalle: %3 Alle: %4 + + + + ShiftBusyModel + + + Job + Servizio + + + + From + Dalle + + + + To + Alle + + + + ShiftGraphEditor + + + Save + Salva + + + + Print + Stampa + + + + Options + Opzioni + + + + Refresh + Ricarica + + + + Shift Graph Editor + Editor Grafico Turni + + + + Save Shift Graph + Salva Grafico Turni + + + + ShiftGraphHolder + + + + Shift %1 + Turno %1 + + + + ShiftJobsModel + + + Job + Servizio + + + + Departure + Partenza + + + + Origin + Stazione di Partenza + + + + Arrival + Arrivo + + + + Destination + Destinazione + + + + ShiftManager + + + New + Nuovo + + + + Remove + Rimuovi + + + + View Shift + Vedi Turno + + + + Sheet + Foglio + + + + Graph + Grafico + + + + Shift Manager + Manager Turno + + + + Error Adding Shift + Errore nell'aggiungere Turno + + + + An error occurred while adding a new shift: +%1 + Errore nell'aggiungere un nuovo turno: +%1 + + + + Save Shift Sheet + Salva Foglio Turno + + + + shift_%1.odt + turno_%1.odt + + + + ShiftSQLModel + + + Shift Name + Nome Turno + + + + There is already another job shift with same name: <b>%1</b> + Esiste già un altro turno con lo stesso nome: <b>%1</b> + + + + ShiftViewer + + + Show in Job Editor + Mostra nell'Editor Servizio + + + + StationFreeRSModel + + + Name + Nome + + + + Free from + Libero dalle + + + + Up to + Fino alle + + + + Job A + Servizio A + + + + Job B + Servizio B + + + + StationFreeRSViewer + + + Refresh + Ricarica + + + + Time: + Orario: + + + + Previous Operation + Operazione precedente + + + + Next Operation + Operazione successiva + + + + Free Rollingstock in %1 + Rotabili liberi a %1 + + + + + Error + Errore + + + + + Database error. Try again. + Errore del database. Riprova. + + + + + No Operation Found + Nessuna operazione trovata + + + + No operation found in station %1 after %2! + Nessuna operazione trovata nella stazione di %1 dopo le %2! + + + + No operation found in station %1 before %2! + Nessuna operazione trovata nella stazione di %1 prima delle %2! + + + + Show RS Plan + Mostra Piano rotabile + + + + Show Job A in JobEditor + Mostra Servizio A nell'Editor Servizi + + + + Show Job B in JobEditor + Mostra Servizio B nell'Editor Servizi + + + + StationJobView + + + Update + Aggiorna + + + + Save sheet + Salva foglio + + + + Show in Job Editor + Mostra nell'Editor Servizio + + + + Show job in graph + Mostra nel grafico servizi + + + + Save Station Sheet + Salva Foglio Stazione + + + + %1_station.odt + stazione_%1.odt + + + + StationLinesListModel + + + Name + Nome + + + + StationPlanModel + + + Arrival + Arrivo + + + + + Departure + Partenza + + + + Platform + Binario + + + + Job + Servizio + + + + Notes + Note + + + + Transit + Transito + + + + StationsManager + + + Stations + Stazioni + + + + Lines + Linee + + + + + Add + Aggiungi + + + + + Remove + Rimuovi + + + + Plan + Trovare traduzione migliore + Piano giornaliero + + + + Free RS + Rotabili liberi + + + + Error Adding Station + Errore nell'aggiungere Stazione + + + + An error occurred while adding a new station: +%1 + Errore nella creazione di una nuova stazione: +%1 + + + + Station error + Errore stazione + + + + Error Adding Line + Errore nell'aggiungere Linea + + + + An error occurred while adding a new line: +%1 + Errore nella creazione di una nuova linea: +%1 + + + + + Edit + Modifica + + + + Stations Manager + Manager Stazioni + + + + StationsSQLModel + + + The name <b>%1</b> is already used by another station.<br>Please choose a different name for each station. + Il nome <b>%1</b> è già utilizzato da un'altra stazione.<br>Per favore scegli un nome differente per ogni stazione. + + + + The name <b>%1</b> is already used as short name for station <b>%2</b>.<br>Please choose a different name for each station. + Il nome <b>%1</b> è già usato come nome abbreviato per la stazione <b>%2</b>.<br>Per favore scegli un nome differente per ogni stazione. + + + + Name and short name cannot be equal (<b>%1</b>). + Il nome ed il nome abbreviato non possono essere uguali (<b>%1</b>). + + + + Name + Nome + + + + Short Name + Abbreviazione + + + + Platforms + Binari + + + + Depots + Depositi + + + + Main Color + Colore principale + + + + Freight Plaftorm + Binario Merci + + + + Passenger Platf + Binario Passeggeri + + + + Default platform for newly created stops when job category is Freight, Postal or Engine movement + Binario di default per le nuove fermate nei treni di categoria Merci, Postale o LIS + + + + Default platform for newly created stops when job is passenger train + Binario di default per le nuove fermate nei treni passeggeri + + + + + Error: %1 + Errore: %1 + + + + StopDelegate + + + Line: %1 + Linea: %1 + + + + StopEditor + + + Station name + Stazione + + + + Press shift if you don't want to change also departure time. + Premi Shift se non vuoi modificare anche l'orario di partenza. + + + + Previous page + Pagina precedente + + + + Next page + Pagina successiva + + + + Page: %1/%2 + Pagina: %1/%2 + + + diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt new file mode 100644 index 0000000..5bb7fd7 --- /dev/null +++ b/src/utils/CMakeLists.txt @@ -0,0 +1,30 @@ +add_subdirectory(spinbox) +add_subdirectory(sqldelegate) +add_subdirectory(thread) + +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + utils/colorview.h + utils/directiontype.h + utils/file_format_names.h + utils/imageviewer.h + utils/jobcategorystrings.h + utils/kmutils.h + utils/rs_types_names.h + utils/rs_utils.h + utils/session_rs_modes.h + utils/types.h + utils/checkproxymodel.h + utils/combodelegate.h + utils/model_roles.h + utils/platform_utils.h + utils/worker_event_types.h + + utils/checkproxymodel.cpp + utils/colorview.cpp + utils/combodelegate.cpp + utils/imageviewer.cpp + utils/kmutils.cpp + utils/rs_utils.cpp + PARENT_SCOPE +) diff --git a/src/utils/checkproxymodel.cpp b/src/utils/checkproxymodel.cpp new file mode 100644 index 0000000..1bc3132 --- /dev/null +++ b/src/utils/checkproxymodel.cpp @@ -0,0 +1,412 @@ +#include "checkproxymodel.h" + +CheckProxyModel::CheckProxyModel(QObject *parent) : + QAbstractListModel(parent), + sourceModel(nullptr), + atLeastACheck(false) +{ + +} + +QVariant CheckProxyModel::data(const QModelIndex &index, int role) const +{ + if(index.isValid() && index.row() < rowCount() && sourceModel) + { + if(role == Qt::CheckStateRole) + { + return m_checks.at(index.row()) ? Qt::Checked : Qt::Unchecked; + } + return sourceModel->data(index, role); + } + return QVariant(); +} + +bool CheckProxyModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if(index.isValid() && index.row() < rowCount() && role == Qt::CheckStateRole) + { + bool val = value.value() == Qt::Checked; + + m_checks[index.row()] = val; + //If it has already other checks don't emit signal + if(val && !atLeastACheck) + { + //New check + atLeastACheck = true; + emit hasCheck(true); + } + else if(atLeastACheck && !m_checks.contains(true)) + { + //Had a check but now it doesn't anymore + atLeastACheck = false; + emit hasCheck(false); + } + + emit dataChanged(index, index); + return true; + } + return false; +} + +int CheckProxyModel::rowCount(const QModelIndex &parent) const +{ + return sourceModel ? sourceModel->rowCount(parent) : 0; +} + +Qt::ItemFlags CheckProxyModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + + return Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemNeverHasChildren; +} + +QVariant CheckProxyModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(sourceModel) + return sourceModel->headerData(section, orientation, role); + return QVariant(); +} + +void CheckProxyModel::setSourceModel(QAbstractItemModel *value) +{ + beginResetModel(); + + if(sourceModel) + { + disconnect(sourceModel, &QAbstractItemModel::dataChanged, this, &CheckProxyModel::onSourceDataChanged); + disconnect(sourceModel, &QAbstractItemModel::headerDataChanged, this, &QAbstractItemModel::headerDataChanged); + disconnect(sourceModel, &QAbstractItemModel::rowsInserted, this, &CheckProxyModel::onSourceRowsInserted); + disconnect(sourceModel, &QAbstractItemModel::rowsRemoved, this, &CheckProxyModel::onRowsRemoved); + disconnect(sourceModel, &QAbstractItemModel::rowsMoved, this, &CheckProxyModel::onRowsMoved); + disconnect(sourceModel, &QAbstractItemModel::modelReset, this, &CheckProxyModel::onModelReset); + } + sourceModel = value; + atLeastACheck = false; + if(sourceModel) + { + connect(sourceModel, &QAbstractItemModel::dataChanged, this, &CheckProxyModel::onSourceDataChanged); + connect(sourceModel, &QAbstractItemModel::headerDataChanged, this, &QAbstractItemModel::headerDataChanged); + connect(sourceModel, &QAbstractItemModel::rowsInserted, this, &CheckProxyModel::onSourceRowsInserted); + connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, &CheckProxyModel::onRowsRemoved); + connect(sourceModel, &QAbstractItemModel::rowsMoved, this, &CheckProxyModel::onRowsMoved); + connect(sourceModel, &QAbstractItemModel::modelReset, this, &CheckProxyModel::onModelReset); + m_checks.fill(false, sourceModel->rowCount()); + m_checks.squeeze(); + } + + endResetModel(); +} + +QAbstractItemModel *CheckProxyModel::getSourceModel() const +{ + return sourceModel; +} + +bool CheckProxyModel::hasAtLeastACheck() const +{ + return atLeastACheck; +} + +QVector CheckProxyModel::checks() const +{ + return m_checks; +} + +void CheckProxyModel::selectAll() +{ + beginResetModel(); + m_checks.fill(true); + endResetModel(); + + if(!atLeastACheck) + { + atLeastACheck = true; + emit hasCheck(true); + } +} + +void CheckProxyModel::selectNone() +{ + beginResetModel(); + m_checks.fill(false); + endResetModel(); + + if(atLeastACheck) + { + atLeastACheck = false; + emit hasCheck(false); + } +} + +void CheckProxyModel::onSourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()) +{ + //Map QModelIndex + emit dataChanged(index(topLeft.row(), topLeft.column()), + index(bottomRight.row(), bottomRight.column()), + roles); +} + +void CheckProxyModel::onSourceRowsInserted(const QModelIndex &parent, int first, int last) +{ + Q_UNUSED(parent); + beginInsertRows(QModelIndex(), first, last); + m_checks.insert(first, last - first + 1, false); + endInsertRows(); +} + +void CheckProxyModel::onRowsRemoved(const QModelIndex &parent, int first, int last) +{ + Q_UNUSED(parent); + + bool checkRemoved = false; + for(int i = first; i < last; i++) + { + if(m_checks.at(i)) + { + checkRemoved = true; + break; + } + } + + beginRemoveRows(QModelIndex(), first, last); + m_checks.remove(first, last - first + 1); + endRemoveRows(); + + if(checkRemoved) + { + if(atLeastACheck && !m_checks.contains(true)) + { + //Had a check but now it doesn't anymore + atLeastACheck = false; + emit hasCheck(false); + } + } +} + +void CheckProxyModel::onRowsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row) +{ + Q_UNUSED(parent); + Q_UNUSED(destination); + + beginMoveRows(QModelIndex(), start, end, QModelIndex(), row); + + const int count = end - start + 1; + for(int i = 0; i < count; i++) + { + m_checks.move(start + i, row + i); + } + + endMoveRows(); +} + +void CheckProxyModel::onModelReset() +{ + selectNone(); +} + +void CheckProxyModel::setChecks(QSet rowSet) +{ + beginResetModel(); + for(int r : rowSet) + { + if(r >= sourceModel->rowCount()) + continue; + + m_checks[r] = true; + } + endResetModel(); + + if(!rowSet.isEmpty() && !atLeastACheck) + { + atLeastACheck = true; + emit hasCheck(true); + } + +} + + +// New interface + +//CheckProxyModel::CheckProxyModel(QObject *parent) : +// QIdentityProxyModel(parent), +// atLeastACheck(false) +//{ +// connect(this, &QIdentityProxyModel::modelReset, this, &CheckProxyModel::selectNone); +// connect(this, &QIdentityProxyModel::rowsInserted, this, &CheckProxyModel::onSourceRowsIserted); +// connect(this, &QIdentityProxyModel::rowsMoved, this, &CheckProxyModel::onSourceRowsMoved); +// connect(this, &QIdentityProxyModel::rowsRemoved, this, &CheckProxyModel::onSourceRowsRemoved); +//} + +//QVariant CheckProxyModel::data(const QModelIndex &index, int role) const +//{ +// if(index.column() == 0 && index.row() < rowCount()) +// { +// if(role == Qt::CheckStateRole) +// { +// return m_checks.at(index.row()) ? Qt::Checked : Qt::Unchecked; +// } +// } +// return QIdentityProxyModel::data(index, role); +//} + +//bool CheckProxyModel::setData(const QModelIndex &index, const QVariant &value, int role) +//{ +// if(index.isValid() && index.row() < rowCount() && role == Qt::CheckStateRole) +// { +// bool val = value.value() == Qt::Checked; + +// m_checks[index.row()] = val; +// //If it has already other checks don't emit signal +// if(val && !atLeastACheck) +// { +// //New check +// atLeastACheck = true; +// emit hasCheck(true); +// } +// else if(atLeastACheck && !m_checks.contains(true)) +// { +// //Had a check but now it doesn't anymore +// atLeastACheck = false; +// emit hasCheck(false); +// } + +// emit dataChanged(index, index); +// return true; +// } +// return false; +//} + +//Qt::ItemFlags CheckProxyModel::flags(const QModelIndex &index) const +//{ +// Qt::ItemFlags f = QIdentityProxyModel::flags(index); +// if(f != 0 && index.column() == 0) +// { +// f.setFlag(Qt::ItemIsUserCheckable); +// } + +// return f; +//} + +//bool CheckProxyModel::hasAtLeastACheck() const +//{ +// return atLeastACheck; +//} + +//void CheckProxyModel::setChecks(QSet rowSet) +//{ +// int count = rowCount(); + +// if(count == 0) +// { +// return; +// } + +// beginResetModel(); + +// for(int r : rowSet) +// { +// if(r >= count) +// continue; + +// m_checks[r] = true; +// } + +// endResetModel(); + +// if(!rowSet.isEmpty() && !atLeastACheck) +// { +// atLeastACheck = true; +// emit hasCheck(true); +// } +//} + +//QVector CheckProxyModel::checks() const +//{ +// return m_checks; +//} + +//void CheckProxyModel::selectAll() +//{ +// int count = rowCount(); + +// beginResetModel(); +// if(count) +// { +// m_checks.fill(true, count); +// } +// else +// { +// m_checks.clear(); +// m_checks.squeeze(); +// } +// endResetModel(); + +// if(!atLeastACheck && count) +// { +// atLeastACheck = true; +// emit hasCheck(true); +// } +//} + +//void CheckProxyModel::selectNone() +//{ +// int count = rowCount(); + +// beginResetModel(); +// if(count) +// { +// m_checks.fill(false, count); +// } +// else +// { +// m_checks.clear(); +// m_checks.squeeze(); +// } +// endResetModel(); + +// if(atLeastACheck) +// { +// atLeastACheck = false; +// emit hasCheck(false); +// } +//} + +//void CheckProxyModel::onSourceRowsIserted(QModelIndex, int first, int last) +//{ +// m_checks.insert(first, last - first + 1, false); +//} + +//void CheckProxyModel::onSourceRowsMoved(QModelIndex, int start, int end, QModelIndex, int row) +//{ +// const int count = end - start + 1; +// for(int i = 0; i < count; i++) +// { +// m_checks.move(start + i, row + i); +// } +//} + +//void CheckProxyModel::onSourceRowsRemoved(QModelIndex, int first, int last) +//{ +// bool checkRemoved = false; +// for(int i = first; i < last; i++) +// { +// if(m_checks.at(i)) +// { +// checkRemoved = true; +// break; +// } +// } + +// m_checks.remove(first, last - first + 1); + +// if(checkRemoved) +// { +// if(atLeastACheck && !m_checks.contains(true)) +// { +// //Had a check but now it doesn't anymore +// atLeastACheck = false; +// emit hasCheck(false); +// } +// } +//} diff --git a/src/utils/checkproxymodel.h b/src/utils/checkproxymodel.h new file mode 100644 index 0000000..27a35d6 --- /dev/null +++ b/src/utils/checkproxymodel.h @@ -0,0 +1,94 @@ +#ifndef CHECKPROXYMODEL_H +#define CHECKPROXYMODEL_H + +#include +#include +#include + +#include + +class CheckProxyModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit CheckProxyModel(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; + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + void setSourceModel(QAbstractItemModel *value); + QAbstractItemModel *getSourceModel() const; + + bool hasAtLeastACheck() const; + + QVector checks() const; + + void setChecks(QSet rowSet); + +signals: + void hasCheck(bool check); + +public slots: + void selectAll(); + void selectNone(); + +private slots: + void onSourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); + void onSourceRowsInserted(const QModelIndex &parent, int first, int last); + void onRowsRemoved(const QModelIndex &parent, int first, int last); + void onRowsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row); + void onModelReset(); + //TODO: what about layoutChanged() ??? + +private: + QAbstractItemModel *sourceModel; + QVector m_checks; + bool atLeastACheck; +}; + + +//BIG TODO: new model crashes after invokeMethod 'resetInternalData()' + +//TODO: maybe handle tree models??? +//class CheckProxyModel : public QIdentityProxyModel +//{ +// Q_OBJECT +//public: +// explicit CheckProxyModel(QObject *parent = nullptr); + +// QVariant data(const QModelIndex &index, int role) const override; +// bool setData(const QModelIndex &index, const QVariant &value, int role) override; + +// Qt::ItemFlags flags(const QModelIndex &index) const override; + +// bool hasAtLeastACheck() const; + +// void setChecks(QSet rowSet); + +// QVector checks() const; + +//signals: +// void hasCheck(bool check); + +//public slots: +// void selectAll(); +// void selectNone(); + +//private slots: +// void onSourceRowsIserted(QModelIndex, int first, int last); +// void onSourceRowsMoved(QModelIndex, int start, int end, QModelIndex, int row); +// void onSourceRowsRemoved(QModelIndex, int first, int last); + +//private: +// QVector m_checks; +// bool atLeastACheck; +//}; + +#endif // CHECKPROXYMODEL_H diff --git a/src/utils/colorview.cpp b/src/utils/colorview.cpp new file mode 100644 index 0000000..fb00d36 --- /dev/null +++ b/src/utils/colorview.cpp @@ -0,0 +1,46 @@ +#include "colorview.h" +#include + +#include +#include + +ColorView::ColorView(QWidget *parent) : + QWidget(parent) +{ + setMinimumSize(30, 30); +} + +void ColorView::setColor(const QColor &color, bool user) +{ + if(mColor == color) + return; + + mColor = color; + + if(user) + { + emit colorChanged(mColor); + } +} + +void ColorView::openColorDialog() +{ + QColor col = QColorDialog::getColor(mColor, + this, + tr("Choose a color")); + setColor(col, true); + + emit editingFinished(); +} + +void ColorView::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.fillRect(rect(), mColor); +} + +void ColorView::mousePressEvent(QMouseEvent *e) +{ + e->accept(); + openColorDialog(); +} diff --git a/src/utils/colorview.h b/src/utils/colorview.h new file mode 100644 index 0000000..624ec16 --- /dev/null +++ b/src/utils/colorview.h @@ -0,0 +1,33 @@ +#ifndef COLORVIEW_H +#define COLORVIEW_H + +#include + +class ColorView : public QWidget +{ + Q_OBJECT +public: + explicit ColorView(QWidget *parent = nullptr); + + inline QColor color() const + { + return mColor; + } + + void setColor(const QColor &color, bool user = false); + + void openColorDialog(); + +signals: + void colorChanged(const QColor&); + void editingFinished(); + +protected: + void paintEvent(QPaintEvent *) override; + void mousePressEvent(QMouseEvent *e) override; + +private: + QColor mColor; +}; + +#endif // COLORVIEW_H diff --git a/src/utils/combodelegate.cpp b/src/utils/combodelegate.cpp new file mode 100644 index 0000000..9ea7108 --- /dev/null +++ b/src/utils/combodelegate.cpp @@ -0,0 +1,64 @@ +#include "combodelegate.h" +#include "app/scopedebug.h" + +#include + +ComboDelegate::ComboDelegate(QStringList list, int role, QObject *parent) : + QStyledItemDelegate(parent), + mList(list), + mRole(role) +{ + //DEBUG_ENTRY; + qDebug() << mList; +} + +QWidget *ComboDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &/*option*/, const QModelIndex &/*index*/) const +{ + //DEBUG_ENTRY; + QComboBox *combo = new QComboBox(parent); + connect(combo, static_cast(&QComboBox::activated), this, &ComboDelegate::onItemClicked); + combo->addItems(mList); + return combo; +} + +void ComboDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + //DEBUG_ENTRY; + QComboBox *combo = static_cast(editor); + QVariant v = index.model()->data(index, mRole); + if(v.isValid()) + { + int val = v.toInt(); + combo->setCurrentIndex(val); + } + else + { + combo->setCurrentIndex(0); //TODO??? -1 --> 0 ? + } + combo->showPopup(); +} + +void ComboDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + //DEBUG_ENTRY; + QComboBox *combo = static_cast(editor); + int val = combo->currentIndex(); + model->setData(index, val, mRole); + //QString text = combo->currentText(); + //model->setData(index, text, Qt::DisplayRole); +} + +void ComboDelegate::onItemClicked() +{ + QComboBox *combo = qobject_cast(sender()); + if(combo) + { + commitData(combo); + closeEditor(combo); + } +} + +void ComboDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &/*index*/) const +{ + editor->setGeometry(option.rect); +} diff --git a/src/utils/combodelegate.h b/src/utils/combodelegate.h new file mode 100644 index 0000000..14614f4 --- /dev/null +++ b/src/utils/combodelegate.h @@ -0,0 +1,31 @@ +#ifndef COMBODELEGATE_H +#define COMBODELEGATE_H + +#include +#include + +class ComboDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + ComboDelegate(QStringList list, int role, QObject *parent = nullptr); + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override; + + void updateEditorGeometry(QWidget *editor, + const QStyleOptionViewItem &option, const QModelIndex &index) const override; + +private slots: + void onItemClicked(); + +private: + QStringList mList; + int mRole; +}; + +#endif // COMBODELEGATE_H diff --git a/src/utils/directiontype.h b/src/utils/directiontype.h new file mode 100644 index 0000000..54f7cf2 --- /dev/null +++ b/src/utils/directiontype.h @@ -0,0 +1,30 @@ +#ifndef DIRECTIONTYPE_H +#define DIRECTIONTYPE_H + +#include + +enum class Direction: bool +{ + Left = 0, + Right = 1 +}; + +static const char* DirectionNamesTable[] = { + QT_TRANSLATE_NOOP("DirectionNames", "Left"), + QT_TRANSLATE_NOOP("DirectionNames", "Right") +}; + +class DirectionNames +{ + Q_DECLARE_TR_FUNCTIONS(DirectionNames) + +public: + static inline QString name(Direction d) + { + if(size_t(d) >= sizeof (DirectionNamesTable)/sizeof (DirectionNamesTable[0])) + return QString(); + return tr(DirectionNamesTable[int(d)]); + } +}; + +#endif // DIRECTIONTYPE_H diff --git a/src/utils/file_format_names.h b/src/utils/file_format_names.h new file mode 100644 index 0000000..bbae757 --- /dev/null +++ b/src/utils/file_format_names.h @@ -0,0 +1,22 @@ +#ifndef FILE_FORMAT_NAMES_H +#define FILE_FORMAT_NAMES_H + +#include + +class FileFormats +{ + Q_DECLARE_TR_FUNCTIONS(FileFormats) + +public: + //BIG TODO: maybe use mimetypes so we automatically get the name from system + //TODO: add custom extension that in future will be registered with an Icon (*.ttt) + static constexpr const char *allFiles = QT_TRANSLATE_NOOP("FileFormats", "All Files (*.*)"); + static constexpr const char *odsFormat = QT_TRANSLATE_NOOP("FileFormats", "OpenDocument Sheet (*.ods)"); + static constexpr const char *odtFormat = QT_TRANSLATE_NOOP("FileFormats", "OpenDocument Text (*.odt)"); + static constexpr const char *tttFormat = QT_TRANSLATE_NOOP("FileFormats", "Train Timetable Session (*.ttt)"); + static constexpr const char *sqliteFormat = QT_TRANSLATE_NOOP("FileFormats", "SQLite 3 Database (*.db *.sqlite *.sqlite3 *.db3)"); + static constexpr const char *svgFile = QT_TRANSLATE_NOOP("FileFormats", "SVG vectorial image (*.svg)"); + static constexpr const char *pdfFile = QT_TRANSLATE_NOOP("FileFormats", "Portable Document Format (*.pdf)"); +}; + +#endif // FILE_FORMAT_NAMES_H diff --git a/src/utils/imageviewer.cpp b/src/utils/imageviewer.cpp new file mode 100644 index 0000000..2c343b4 --- /dev/null +++ b/src/utils/imageviewer.cpp @@ -0,0 +1,93 @@ +#include "imageviewer.h" + +#include +#include +#include +#include + +ImageViewer::ImageViewer(QWidget *parent) : + QDialog(parent) +{ + QVBoxLayout *lay = new QVBoxLayout(this); + + infoLabel = new QLabel; + lay->addWidget(infoLabel); + + slider = new QSlider(Qt::Horizontal); + lay->addWidget(slider); + + scrollArea = new QScrollArea; + lay->addWidget(scrollArea); + + imageLabel = new QLabel; + imageLabel->setBackgroundRole(QPalette::Base); + + scrollArea->setWidget(imageLabel); + scrollArea->setBackgroundRole(QPalette::Dark); + scrollArea->setAlignment(Qt::AlignCenter); + + slider->setRange(1, 200); + connect(slider, &QSlider::valueChanged, this, &ImageViewer::setScale); + + setMinimumSize(200, 200); +} + +void ImageViewer::setImage(const QImage &img) +{ + originalImg = img; + + + const int widthPx = img.width(); + const int heightPx = img.height(); + const int dotsX = img.dotsPerMeterX(); + const int dotsY = img.dotsPerMeterY(); + + const double widthCm = 100.0 * double(widthPx) / double(dotsX); + const double heightCm = 100.0 * double(heightPx) / double(dotsY); + + QString info; + info.append(QStringLiteral("Size: %1px / %2px\n").arg(widthPx).arg(heightPx)); + info.append(QStringLiteral("Width: %1 cm\n").arg(widthCm, 0, 'f', 2)); + info.append(QStringLiteral("Height: %1 cm\n").arg(heightCm, 0, 'f', 2)); + + QStringList keys = img.textKeys(); + for(const QString& key : keys) + { + info.append("'"); + info.append(key); + info.append("' = '"); + info.append(img.text(key)); + info.append("'\n"); + } + infoLabel->setText(info); + + setScale(100); + + if(originalImg.isNull()) + { + imageLabel->setPixmap(QPixmap()); + imageLabel->setText(tr("No image")); + imageLabel->adjustSize(); + } +} + +void ImageViewer::setScale(int val) +{ + if(originalImg.isNull()) + return; + + slider->setValue(val); + QString tip = QStringLiteral("%1%").arg(val); + imageLabel->setToolTip(tip); + slider->setToolTip(tip); + + if(val == 100) + { + scaledImg = originalImg; + }else{ + scaledImg = originalImg.scaledToWidth(originalImg.width() * val / 100, Qt::SmoothTransformation); + } + + imageLabel->setPixmap(QPixmap::fromImage(scaledImg)); + imageLabel->adjustSize(); +} diff --git a/src/utils/imageviewer.h b/src/utils/imageviewer.h new file mode 100644 index 0000000..9a325f1 --- /dev/null +++ b/src/utils/imageviewer.h @@ -0,0 +1,33 @@ +#ifndef IMAGEVIEWER_H +#define IMAGEVIEWER_H + +#include +#include + +class QScrollArea; +class QSlider; +class QLabel; + +class ImageViewer : public QDialog +{ + Q_OBJECT +public: + ImageViewer(QWidget *parent = nullptr); + virtual ~ImageViewer() {} + + void setImage(const QImage& img); + +public slots: + void setScale(int val); + +private: + QImage originalImg; + QImage scaledImg; + + QSlider *slider; + QScrollArea *scrollArea; + QLabel *infoLabel; + QLabel *imageLabel; +}; + +#endif // IMAGEVIEWER_H diff --git a/src/utils/jobcategorystrings.h b/src/utils/jobcategorystrings.h new file mode 100644 index 0000000..7c1d94c --- /dev/null +++ b/src/utils/jobcategorystrings.h @@ -0,0 +1,65 @@ +#ifndef JOBCATEGORYSTRINGS_H +#define JOBCATEGORYSTRINGS_H + +#include +#include "types.h" + +static const char* JobCategoryFullNameTable[int(JobCategory::NCategories)] = { //NOTE: keep in sync wiht JobCategory + QT_TRANSLATE_NOOP("JobCategoryName", "FREIGHT"), + QT_TRANSLATE_NOOP("JobCategoryName", "LIS"), + QT_TRANSLATE_NOOP("JobCategoryName", "POSTAL"), + + QT_TRANSLATE_NOOP("JobCategoryName", "REGIONAL"), + QT_TRANSLATE_NOOP("JobCategoryName", "FAST REGIONAL"), + QT_TRANSLATE_NOOP("JobCategoryName", "LOCAL"), + QT_TRANSLATE_NOOP("JobCategoryName", "INTERCITY"), + QT_TRANSLATE_NOOP("JobCategoryName", "EXPRESS"), + QT_TRANSLATE_NOOP("JobCategoryName", "DIRECT"), + QT_TRANSLATE_NOOP("JobCategoryName", "HIGH SPEED") +}; + +static const char* JobCategoryAbbrNameTable[int(JobCategory::NCategories)] = { //NOTE: keep in sync wiht JobCategory + QT_TRANSLATE_NOOP("JobCategoryName", "FRG"), + QT_TRANSLATE_NOOP("JobCategoryName", "LIS"), + QT_TRANSLATE_NOOP("JobCategoryName", "P"), + + QT_TRANSLATE_NOOP("JobCategoryName", "R"), + QT_TRANSLATE_NOOP("JobCategoryName", "RF"), + QT_TRANSLATE_NOOP("JobCategoryName", "LOC"), + QT_TRANSLATE_NOOP("JobCategoryName", "IC"), + QT_TRANSLATE_NOOP("JobCategoryName", "EXP"), + QT_TRANSLATE_NOOP("JobCategoryName", "DIR"), + QT_TRANSLATE_NOOP("JobCategoryName", "HSP") +}; + +namespace JobCategoryName__ { +constexpr char unknownCatName[] = "Unknown"; +} + + +class JobCategoryName +{ + Q_DECLARE_TR_FUNCTIONS(JobCategoryName) + +public: + static inline QString fullName(JobCategory cat) + { + if(cat >= JobCategory::NCategories) + return JobCategoryName__::unknownCatName; + return tr(JobCategoryFullNameTable[int(cat)]); + } + + static inline QString shortName(JobCategory cat) + { + if(cat >= JobCategory::NCategories) + return JobCategoryName__::unknownCatName; + return tr(JobCategoryAbbrNameTable[int(cat)]); + } + + static inline QString jobName(db_id jobId, JobCategory cat) + { + return shortName(cat) + QString::number(jobId); //Example: LIS1234 + } +}; + +#endif // JOBCATEGORYSTRINGS_H diff --git a/src/utils/kmutils.cpp b/src/utils/kmutils.cpp new file mode 100644 index 0000000..ef7e022 --- /dev/null +++ b/src/utils/kmutils.cpp @@ -0,0 +1,38 @@ +#include "kmutils.h" + +#include + +QString utils::kmNumToText(int kmInMeters) +{ + //Add last digit and '+', at least 5 char (X+XXX) + int numberLen = qMax(5, int(floor(log10(kmInMeters))) + 2); + QString str(numberLen, QChar('0')); + + for(int i = 0; i < numberLen; i++) + { + if(i == 3) + { + str[str.size() - 4] = '+'; + continue; + } + int rem = kmInMeters % 10; + str[str.size() - i - 1] = QChar('0' + rem); + kmInMeters = (kmInMeters - rem) / 10; + } + return str; +} + +int utils::kmNumFromTextInMeters(const QString &str) +{ + int kmInMeters = 0; + for(int i = 0; i < str.size(); i++) + { + QChar ch = str.at(i); + if(ch.isDigit()) + { + kmInMeters *= 10; + kmInMeters += ch.digitValue(); + } + } + return kmInMeters; +} diff --git a/src/utils/kmutils.h b/src/utils/kmutils.h new file mode 100644 index 0000000..335ee25 --- /dev/null +++ b/src/utils/kmutils.h @@ -0,0 +1,21 @@ +#ifndef KMUTILS_H +#define KMUTILS_H + +#include + +namespace utils { + +/* Convinence functions: + * Km in railways are expressed with fixed decimal point + * Decimal separator: '+' + * Deciaml digits: 3 (meters) + * Format: X+XXX + * + * Example: 15.75 km -> KM 15+750 + */ +QString kmNumToText(int kmInMeters); +int kmNumFromTextInMeters(const QString& str); + +} //namespace utils + +#endif // KMUTILS_H diff --git a/src/utils/model_mode.h b/src/utils/model_mode.h new file mode 100644 index 0000000..710f1f2 --- /dev/null +++ b/src/utils/model_mode.h @@ -0,0 +1,14 @@ +#ifndef MODEL_MODE_H +#define MODEL_MODE_H + +namespace ModelModes { +enum Mode +{ + Owners, + Models, + Rollingstock +}; //FIXME: maybe remove +} + + +#endif // MODEL_MODE_H diff --git a/src/utils/model_roles.h b/src/utils/model_roles.h new file mode 100644 index 0000000..3299f59 --- /dev/null +++ b/src/utils/model_roles.h @@ -0,0 +1,70 @@ +#ifndef MODEL_ROLES_H +#define MODEL_ROLES_H + +#include + +//Useful constants to set/retrive data from models + +#define STOP_ID (Qt::UserRole) +#define STATION_ID (Qt::UserRole + 1) +#define STATION_NAME (Qt::DisplayRole) + +#define ROW_ID (Qt::UserRole + 2) + +#define RS_ID (Qt::UserRole + 3) + +#define LINE_ID (Qt::UserRole) +#define START_STATION_ID (Qt::UserRole + 1) +#define END_STATION_ID (Qt::UserRole + 2) + +#define PLATF_ID (Qt::UserRole + 4) + +#define DIRECTION_ROLE (Qt::UserRole + 5) + +#define JOB_SHIFT_ID (Qt::UserRole + 6) + +#define DISTANCE_ROLE (Qt::UserRole + 7) + +#define INDEX_ROLE (Qt::UserRole + 8) + +#define KMPOS_ROLE (Qt::UserRole + 9) + +#define JOB_ID_ROLE (Qt::UserRole + 10) + +#define MAX_SPEED_ROLE (Qt::UserRole + 11) + +#define JOB_CATEGORY_ROLE (Qt::UserRole + 12) + +#define JOB_FIRST_ST_ROLE (Qt::UserRole + 13) +#define JOB_FIRST_ST_NAME_ROLE (Qt::UserRole + 14) +#define JOB_LAST_ST_ROLE (Qt::UserRole + 15) +#define JOB_LAST_ST_NAME_ROLE (Qt::UserRole + 16) +#define JOB_SHIFT_NAME (Qt::UserRole + 17) +#define JOB_CATEGORY_NAME_ROLE (Qt::UserRole + 18) + +#define STOP_TYPE_ROLE (Qt::UserRole + 20) +#define ARR_ROLE (Qt::UserRole + 21) +#define DEP_ROLE (Qt::UserRole + 22) +#define STATION_ROLE (Qt::UserRole + 23) +#define CUR_LINE_ROLE (Qt::UserRole + 24) +#define LINES_ROLE (Qt::UserRole + 25) +#define SEGMENT_ROLE (Qt::UserRole + 26) +#define OTHER_SEG_ROLE (Qt::UserRole + 27) +#define NEXT_LINE_ROLE (Qt::UserRole + 28) + +#define POSSIBLE_LINE_ROLE (Qt::UserRole + 29) +#define ADDHERE_ROLE (Qt::UserRole + 30) + +#define STOP_DESCR_ROLE (Qt::UserRole + 31) + + +#define RS_MODEL_ID (Qt::UserRole + 40) +#define RS_TYPE_ROLE (Qt::UserRole + 41) +#define RS_SUB_TYPE_ROLE (Qt::UserRole + 42) +#define RS_OWNER_ID (Qt::UserRole + 43) +#define RS_NUMBER (Qt::UserRole + 44) +#define RS_IS_ENGINE (Qt::UserRole + 45) + +#define COLOR_ROLE (Qt::UserRole + 46) + +#endif // MODEL_ROLES_H diff --git a/src/utils/platform_utils.h b/src/utils/platform_utils.h new file mode 100644 index 0000000..8f5de8e --- /dev/null +++ b/src/utils/platform_utils.h @@ -0,0 +1,26 @@ +#ifndef PLATFORM_UTILS_H +#define PLATFORM_UTILS_H + +#include + +namespace utils { + +inline QString platformName(int platf) +{ + if(platf < 0) //Depot + return QString::number(-platf) + QStringLiteral(" Depot"); + else + return QString::number(platf + 1); //Main Platform +} + +inline QString shortPlatformName(int platf) +{ + if(platf < 0) //Depot + return QString::number(-platf) + QStringLiteral(" Dep"); + else + return QString::number(platf + 1); //Main Platform +} + +} //namespace utils + +#endif // PLATFORM_UTILS_H diff --git a/src/utils/rs_types_names.h b/src/utils/rs_types_names.h new file mode 100644 index 0000000..a49d12e --- /dev/null +++ b/src/utils/rs_types_names.h @@ -0,0 +1,41 @@ +#ifndef RS_TYPES_NAMES_H +#define RS_TYPES_NAMES_H + +#include +#include "types.h" + +static const char* RsTypeNamesTable[] = { + QT_TRANSLATE_NOOP("RsTypeNames", "Engine"), + QT_TRANSLATE_NOOP("RsTypeNames", "Freight Wagon"), + QT_TRANSLATE_NOOP("RsTypeNames", "Coach") +}; + +static const char* RsSubTypeNamesTable[] = { + QT_TRANSLATE_NOOP("RsTypeNames", "Not set!"), + QT_TRANSLATE_NOOP("RsTypeNames", "Electric"), + QT_TRANSLATE_NOOP("RsTypeNames", "Diesel"), + QT_TRANSLATE_NOOP("RsTypeNames", "Steam") +}; + + +class RsTypeNames +{ + Q_DECLARE_TR_FUNCTIONS(RsTypeNames) + +public: + static inline QString name(RsType t) + { + if(t >= RsType::NTypes) + return QString(); + return tr(RsTypeNamesTable[int(t)]); + } + + static inline QString name(RsEngineSubType t) + { + if(t >= RsEngineSubType::NTypes) + return QString(); + return tr(RsSubTypeNamesTable[int(t)]); + } +}; + +#endif // RS_TYPES_NAMES_H diff --git a/src/utils/rs_utils.cpp b/src/utils/rs_utils.cpp new file mode 100644 index 0000000..69bd2bd --- /dev/null +++ b/src/utils/rs_utils.cpp @@ -0,0 +1,121 @@ +#include "rs_utils.h" + +#include + +QString rs_utils::formatNum(RsType type, int number) +{ + if(type == RsType::Engine) + { + return QString::number(number).rightJustified(3, '0'); + } + else + { + //Freight wagons and coaches + QString str = QStringLiteral("%1").arg(number, 4, 10, QChar('0')); + str.insert(3, '-'); // 'XXX-X' + return str; + } +} + +QString rs_utils::formatName(const QString &model, int number, const QString &suffix, RsType type) +{ + QString name = model; + name.reserve(model.size() + suffix.size() + 10); //1 char separator (' ', '.') + 9 digit max + + + int numberLen = int(floor(log10(number))) + 1; + + if(type == RsType::Engine) + { + name.append('.'); //'[Name].XXX[Suffix] + + const int count = qMax(5, numberLen); + for(int i = 0; i < count; i++) + name.append('0'); + for(int i = 0; i < numberLen; i++) + { + int rem = number % 10; + name[name.size() - i - 1] = QChar('0' + rem); + number = (number - rem) / 10; + } + } + else + { + name.append(' '); + + //Freight wagons and coaches '[Name] XXX-X[Suffix]' + numberLen++; + const int count = qMax(5, numberLen); + for(int i = 0; i < count; i++) + name.append('0'); + for(int i = 0; i < numberLen; i++) + { + if(i == 1) + { + name[name.size() - 2] = '-'; + continue; + } + int rem = number % 10; + name[name.size() - i - 1] = QChar('0' + rem); + number = (number - rem) / 10; + } + } + + if(type == RsType::Engine) + { + name.append('.'); + name.append(QString::number(number)); + }else{ + name.append(' '); + + //Freight wagons and coaches + QString str = QStringLiteral("%1").arg(number, 4, 10, QChar('0')); + str.insert(3, '-'); // 'XXX-X' + name.append(str); + } + name.append(suffix); + return name; +} + +QString rs_utils::formatNameRef(const char *model, int modelSize, int number, const char *suffix, int suffixSize, RsType type) +{ + QByteArray name; + name.reserve(modelSize + suffixSize + 10); //1 char separator (' ', '.') + 9 digit max + name.append(model, modelSize); + + int numberLen = int(floor(log10(number))) + 1; + + if(type == RsType::Engine) + { + name.append('.'); //'[Name].XXX[Suffix] + + name.append(qMax(3, numberLen), '0'); + for(int i = 0; i < numberLen; i++) + { + int rem = number % 10; + name[name.size() - i - 1] = char('0' + rem); + number = (number - rem) / 10; + } + } + else + { + name.append(' '); + + //Freight wagons and coaches '[Name] XXX-X[Suffix]' + numberLen++; + name.append(qMax(5, numberLen), '0'); + for(int i = 0; i < numberLen; i++) + { + if(i == 1) + { + name[name.size() - 2] = '-'; + continue; + } + int rem = number % 10; + name[name.size() - i - 1] = char('0' + rem); + number = (number - rem) / 10; + } + } + name.append(suffix, suffixSize); + return QString::fromUtf8(name); +} diff --git a/src/utils/rs_utils.h b/src/utils/rs_utils.h new file mode 100644 index 0000000..ea24729 --- /dev/null +++ b/src/utils/rs_utils.h @@ -0,0 +1,18 @@ +#ifndef RS_UTILS_H +#define RS_UTILS_H + +#include + +#include "types.h" + +namespace rs_utils +{ +//Format RS number according to its type +QString formatNum(RsType type, int number); + +QString formatName(const QString& model, int number, const QString& suffix, RsType type); +QString formatNameRef(const char *model, int modelSize, int number, const char *suffix, int suffixSize, RsType type); + +} + +#endif // RS_UTILS_H diff --git a/src/utils/session_rs_modes.h b/src/utils/session_rs_modes.h new file mode 100644 index 0000000..8f0e489 --- /dev/null +++ b/src/utils/session_rs_modes.h @@ -0,0 +1,16 @@ +#ifndef SESSION_RS_MODES_H +#define SESSION_RS_MODES_H + +enum class SessionRSMode +{ + StartOfSession, + EndOfSession +}; + +enum class SessionRSOrder +{ + ByStation, + ByOwner +}; + +#endif // SESSION_RS_MODES_H diff --git a/src/utils/spinbox/CMakeLists.txt b/src/utils/spinbox/CMakeLists.txt new file mode 100644 index 0000000..5c5c7ed --- /dev/null +++ b/src/utils/spinbox/CMakeLists.txt @@ -0,0 +1,7 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + utils/spinbox/spinboxeditorfactory.h + + utils/spinbox/spinboxeditorfactory.cpp + PARENT_SCOPE +) diff --git a/src/utils/spinbox/spinboxeditorfactory.cpp b/src/utils/spinbox/spinboxeditorfactory.cpp new file mode 100644 index 0000000..b94419f --- /dev/null +++ b/src/utils/spinbox/spinboxeditorfactory.cpp @@ -0,0 +1,51 @@ +#include "spinboxeditorfactory.h" +#include + +SpinBoxEditorFactory::SpinBoxEditorFactory() : + m_minVal(0), + m_maxVal(99) +{ + +} + +QWidget *SpinBoxEditorFactory::createEditor(int /*userType*/, QWidget *parent) const +{ + QSpinBox *spin = new QSpinBox(parent); + spin->setRange(m_minVal, m_maxVal); + spin->setPrefix(m_prefix); + spin->setSuffix(m_suffix); + spin->setSpecialValueText(m_specialValueText); + spin->setAlignment(alignment); + return spin; +} + +QByteArray SpinBoxEditorFactory::valuePropertyName(int) const +{ + return "value"; +} + +void SpinBoxEditorFactory::setRange(int min, int max) +{ + m_minVal = min; + m_maxVal = max; +} + +void SpinBoxEditorFactory::setPrefix(const QString &prefix) +{ + m_prefix = prefix; +} + +void SpinBoxEditorFactory::setSuffix(const QString &suffix) +{ + m_suffix = suffix; +} + +void SpinBoxEditorFactory::setSpecialValueText(const QString &specialValueText) +{ + m_specialValueText = specialValueText; +} + +void SpinBoxEditorFactory::setAlignment(const Qt::Alignment &value) +{ + alignment = value; +} diff --git a/src/utils/spinbox/spinboxeditorfactory.h b/src/utils/spinbox/spinboxeditorfactory.h new file mode 100644 index 0000000..187657f --- /dev/null +++ b/src/utils/spinbox/spinboxeditorfactory.h @@ -0,0 +1,29 @@ +#ifndef SPINBOXEDITORFACTORY_H +#define SPINBOXEDITORFACTORY_H + +#include + +class SpinBoxEditorFactory : public QItemEditorFactory +{ +public: + SpinBoxEditorFactory(); + + QWidget *createEditor(int userType, QWidget *parent) const override; + QByteArray valuePropertyName(int) const override; + + void setRange(int min, int max); + void setPrefix(const QString &prefix); + void setSuffix(const QString &suffix); + void setSpecialValueText(const QString &specialValueText); + void setAlignment(const Qt::Alignment &value); + +private: + int m_minVal; + int m_maxVal; + QString m_prefix; + QString m_suffix; + QString m_specialValueText; + Qt::Alignment alignment; +}; + +#endif // SPINBOXEDITORFACTORY_H diff --git a/src/utils/sqldelegate/CMakeLists.txt b/src/utils/sqldelegate/CMakeLists.txt new file mode 100644 index 0000000..9935445 --- /dev/null +++ b/src/utils/sqldelegate/CMakeLists.txt @@ -0,0 +1,21 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + utils/sqldelegate/IFKField.h + utils/sqldelegate/chooseitemdlg.h + utils/sqldelegate/customcompletionlineedit.h + utils/sqldelegate/imatchmodelfactory.h + utils/sqldelegate/isqlfkmatchmodel.h + utils/sqldelegate/modelpageswitcher.h + utils/sqldelegate/pageditemmodel.h + utils/sqldelegate/sqlfkfielddelegate.h + + utils/sqldelegate/IFKField.cpp + utils/sqldelegate/chooseitemdlg.cpp + utils/sqldelegate/customcompletionlineedit.cpp + utils/sqldelegate/imatchmodelfactory.cpp + utils/sqldelegate/isqlfkmatchmodel.cpp + utils/sqldelegate/modelpageswitcher.cpp + utils/sqldelegate/pageditemmodel.cpp + utils/sqldelegate/sqlfkfielddelegate.cpp + PARENT_SCOPE +) diff --git a/src/utils/sqldelegate/IFKField.cpp b/src/utils/sqldelegate/IFKField.cpp new file mode 100644 index 0000000..fdc26d4 --- /dev/null +++ b/src/utils/sqldelegate/IFKField.cpp @@ -0,0 +1,6 @@ +#include "IFKField.h" + +IFKField::~IFKField() +{ + +} diff --git a/src/utils/sqldelegate/IFKField.h b/src/utils/sqldelegate/IFKField.h new file mode 100644 index 0000000..8c50531 --- /dev/null +++ b/src/utils/sqldelegate/IFKField.h @@ -0,0 +1,22 @@ +#ifndef IFKFIELD_H +#define IFKFIELD_H + +#include +#include "utils/types.h" + +/* IOwnerField + * Generic interface for RSOwnersSQLDelegate so it can be used by multiple models + * Used for set a Foreign Key field: user types the name and the model gets the corresponding id +*/ + +class IFKField +{ +public: + virtual ~IFKField(); + + virtual bool getFieldData(int row, int col, db_id &idOut, QString& nameOut) const = 0; + virtual bool validateData(int row, int col, db_id id, const QString& name) = 0; + virtual bool setFieldData(int row, int col, db_id id, const QString& name) = 0; +}; + +#endif // IFKFIELD_H diff --git a/src/utils/sqldelegate/chooseitemdlg.cpp b/src/utils/sqldelegate/chooseitemdlg.cpp new file mode 100644 index 0000000..376148f --- /dev/null +++ b/src/utils/sqldelegate/chooseitemdlg.cpp @@ -0,0 +1,78 @@ +#include "chooseitemdlg.h" + +#include +#include +#include +#include +#include + +#include "utils/sqldelegate/customcompletionlineedit.h" + +ChooseItemDlg::ChooseItemDlg(ISqlFKMatchModel *matchModel, QWidget *parent) : + QDialog(parent), + itemId(0) +{ + QVBoxLayout *lay = new QVBoxLayout(this); + + label = new QLabel; + lay->addWidget(label); + + lineEdit = new CustomCompletionLineEdit(matchModel); + connect(lineEdit, &CustomCompletionLineEdit::dataIdChanged, this, &ChooseItemDlg::itemChosen); + lay->addWidget(lineEdit); + + buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + lay->addWidget(buttonBox); + + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + itemChosen(0); + + setWindowTitle(tr("Insert value")); + setMinimumSize(250, 100); +} + +void ChooseItemDlg::setDescription(const QString &text) +{ + label->setText(text); +} + +void ChooseItemDlg::setPlaceholder(const QString &text) +{ + lineEdit->setPlaceholderText(text); +} + +void ChooseItemDlg::done(int res) +{ + if(res == QDialog::Accepted) + { + QString errMsg; + if(m_callback && !m_callback(itemId, errMsg)) + { + QMessageBox::warning(this, tr("Error"), errMsg); + return; + } + } + + QDialog::done(res); +} + +void ChooseItemDlg::itemChosen(db_id id) +{ + itemId = id; + QPushButton *okBut = buttonBox->button(QDialogButtonBox::Ok); + if(itemId) + { + okBut->setToolTip(QString()); + okBut->setEnabled(true); + }else{ + okBut->setToolTip(tr("In order to proceed you must select a valid item.")); + okBut->setEnabled(false); + } +} + +void ChooseItemDlg::setCallback(const Callback &callback) +{ + m_callback = callback; +} diff --git a/src/utils/sqldelegate/chooseitemdlg.h b/src/utils/sqldelegate/chooseitemdlg.h new file mode 100644 index 0000000..455708e --- /dev/null +++ b/src/utils/sqldelegate/chooseitemdlg.h @@ -0,0 +1,44 @@ +#ifndef CHOOSEITEMDLG_H +#define CHOOSEITEMDLG_H + +#include + +#include "utils/types.h" + +#include + +class QDialogButtonBox; +class QLabel; +class CustomCompletionLineEdit; +class ISqlFKMatchModel; + +class ChooseItemDlg : public QDialog +{ + Q_OBJECT +public: + typedef std::function Callback; + + ChooseItemDlg(ISqlFKMatchModel *matchModel, QWidget *parent); + + void setDescription(const QString& text); + void setPlaceholder(const QString& text); + + void setCallback(const Callback &callback); + + inline db_id getItemId() const { return itemId; } + +public slots: + void done(int res) override; + +private slots: + void itemChosen(db_id id); + +private: + QLabel *label; + CustomCompletionLineEdit *lineEdit; + QDialogButtonBox *buttonBox; + db_id itemId; + Callback m_callback; +}; + +#endif // CHOOSEITEMDLG_H diff --git a/src/utils/sqldelegate/customcompletionlineedit.cpp b/src/utils/sqldelegate/customcompletionlineedit.cpp new file mode 100644 index 0000000..895cea8 --- /dev/null +++ b/src/utils/sqldelegate/customcompletionlineedit.cpp @@ -0,0 +1,222 @@ +#include "customcompletionlineedit.h" +#include "isqlfkmatchmodel.h" + +#include +#include + +#include +#include + +#include + +CustomCompletionLineEdit::CustomCompletionLineEdit(ISqlFKMatchModel *m, QWidget *parent) : + QLineEdit(parent), + popup(nullptr), + model(m), + dataId(0), + suggestionsTimerId(0) +{ + popup = new QTreeView; + popup->setWindowFlags(Qt::Popup); + popup->setFocusPolicy(Qt::NoFocus); + popup->setFocusProxy(this); + popup->setMouseTracking(true); + popup->header()->hide(); + +#ifdef PRINT_DBG_MSG + popup->setObjectName(QStringLiteral("Popup %1 (%2)").arg(qintptr(popup)).arg(qintptr(this))); + setObjectName(QStringLiteral("CustomCompletionLineEdit (%1)").arg(qintptr(this))); +#endif + + popup->setUniformRowHeights(true); + popup->setRootIsDecorated(false); + popup->setEditTriggers(QAbstractItemView::NoEditTriggers); + popup->setSelectionBehavior(QAbstractItemView::SelectRows); + popup->setFrameStyle(QFrame::Box | QFrame::Plain); + popup->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + popup->installEventFilter(this); + popup->setModel(model); + + connect(popup, &QAbstractItemView::clicked, this, &CustomCompletionLineEdit::doneCompletion); + connect(this, &QLineEdit::textEdited, this, &CustomCompletionLineEdit::startSuggestionsTimer); + connect(model, &ISqlFKMatchModel::resultsReady, this, &CustomCompletionLineEdit::resultsReady); +} + +CustomCompletionLineEdit::~CustomCompletionLineEdit() +{ + if(suggestionsTimerId) + { + killTimer(suggestionsTimerId); + suggestionsTimerId = 0; + } + delete popup; +} + +void CustomCompletionLineEdit::showPopup() +{ + if(popup->isVisible()) + return; + + popup->move(mapToGlobal(QPoint(0, height()))); + popup->resize(width(), height() * 4); + popup->setFocus(); + popup->show(); + model->refreshData(); +} + +bool CustomCompletionLineEdit::getData(db_id &idOut, QString& nameOut) const +{ + idOut = dataId; + nameOut = text(); + + return dataId != 0; +} + +void CustomCompletionLineEdit::setData(db_id id, const QString &name) +{ + if(id && name.isEmpty()) + setText(model->getName(id)); + else + setText(name); + + if(id == dataId) + return; + + dataId = id; + emit dataIdChanged(dataId); +} + +void CustomCompletionLineEdit::timerEvent(QTimerEvent *e) +{ + if(suggestionsTimerId && e->timerId() == suggestionsTimerId) + { + killTimer(suggestionsTimerId); + suggestionsTimerId = 0; + + model->autoSuggest(text()); + + showPopup(); + + return; + } + + QLineEdit::timerEvent(e); +} + +void CustomCompletionLineEdit::mousePressEvent(QMouseEvent *e) +{ + showPopup(); + QLineEdit::mousePressEvent(e); +} + +bool CustomCompletionLineEdit::eventFilter(QObject *obj, QEvent *ev) +{ + if (obj != popup) + return false; + + if (ev->type() == QEvent::MouseButtonPress) + { + popup->hide(); + model->clearCache(); + setFocus(); + return true; + } + + if (ev->type() == QEvent::KeyPress) + { + bool consumed = false; + int key = static_cast(ev)->key(); + switch (key) + { + case Qt::Key_Enter: + case Qt::Key_Return: + doneCompletion(popup->currentIndex()); + consumed = true; + break; + + case Qt::Key_Escape: + setFocus(); + popup->hide(); + model->clearCache(); + consumed = true; + break; + + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_Home: + case Qt::Key_End: + case Qt::Key_PageUp: + case Qt::Key_PageDown: + break; + + default: + setFocus(); + event(ev); + popup->hide(); + model->clearCache(); + break; + } + + return consumed; + } + + return false; +} + +void CustomCompletionLineEdit::doneCompletion(const QModelIndex& idx) +{ + if(suggestionsTimerId) + { + killTimer(suggestionsTimerId); + suggestionsTimerId = 0; + } + + if(idx.row() >= 0 && !model->isEmptyRow(idx.row()) && !model->isEllipsesRow(idx.row())) + { + setData(model->getIdAtRow(idx.row()), model->getNameAtRow(idx.row())); + } + else + { + setData(0, QString()); + } + + popup->hide(); + model->clearCache(); + clearFocus(); + + emit completionDone(this); +} + +void CustomCompletionLineEdit::startSuggestionsTimer() +{ + if(suggestionsTimerId) + killTimer(suggestionsTimerId); + suggestionsTimerId = startTimer(250); +} + +void CustomCompletionLineEdit::resultsReady(bool forceFirst) +{ + resizeColumnToContents(); + selectFirstIndexOrNone(forceFirst); +} + +void CustomCompletionLineEdit::resizeColumnToContents() +{ + const int colCount = model->columnCount(); + for(int i = 0; i < colCount; i++) + popup->resizeColumnToContents(i); +} + +void CustomCompletionLineEdit::selectFirstIndexOrNone(bool forceFirst) +{ + QModelIndex idx; //Invalid index (no item chosen, empty text) + if(dataId || forceFirst) + idx = model->index(0, 0); //Select the chosen item + popup->setCurrentIndex(idx); +} + +void CustomCompletionLineEdit::setData_slot(db_id id) +{ + setData(id, QString()); +} diff --git a/src/utils/sqldelegate/customcompletionlineedit.h b/src/utils/sqldelegate/customcompletionlineedit.h new file mode 100644 index 0000000..707b436 --- /dev/null +++ b/src/utils/sqldelegate/customcompletionlineedit.h @@ -0,0 +1,51 @@ +#ifndef CUSTOMLINEEDIT_H +#define CUSTOMLINEEDIT_H + +#include + +#include "utils/types.h" + +class QTreeView; +class ISqlFKMatchModel; + +class CustomCompletionLineEdit : public QLineEdit +{ + Q_OBJECT +public: + explicit CustomCompletionLineEdit(ISqlFKMatchModel *m, QWidget *parent = nullptr); + ~CustomCompletionLineEdit(); + + void showPopup(); + + bool getData(db_id &idOut, QString &nameOut) const; + + void setData(db_id id, const QString& name = QString()); + + void resizeColumnToContents(); + void selectFirstIndexOrNone(bool forceFirst); + +signals: + void completionDone(CustomCompletionLineEdit *self); + void dataIdChanged(db_id id); + +public slots: + void setData_slot(db_id id); + +protected: + void timerEvent(QTimerEvent *event); + void mousePressEvent(QMouseEvent *e); + bool eventFilter(QObject *obj, QEvent *ev); + +private slots: + void doneCompletion(const QModelIndex &idx); + void startSuggestionsTimer(); + void resultsReady(bool forceFirst); + +private: + QTreeView *popup; + ISqlFKMatchModel *model; + db_id dataId; + int suggestionsTimerId; +}; + +#endif // CUSTOMLINEEDIT_H diff --git a/src/utils/sqldelegate/imatchmodelfactory.cpp b/src/utils/sqldelegate/imatchmodelfactory.cpp new file mode 100644 index 0000000..fd71db0 --- /dev/null +++ b/src/utils/sqldelegate/imatchmodelfactory.cpp @@ -0,0 +1,7 @@ +#include "imatchmodelfactory.h" + +IMatchModelFactory::IMatchModelFactory(QObject *parent) : + QObject(parent) +{ + +} diff --git a/src/utils/sqldelegate/imatchmodelfactory.h b/src/utils/sqldelegate/imatchmodelfactory.h new file mode 100644 index 0000000..81951a6 --- /dev/null +++ b/src/utils/sqldelegate/imatchmodelfactory.h @@ -0,0 +1,17 @@ +#ifndef IMATCHMODELFACTORY_H +#define IMATCHMODELFACTORY_H + +#include + +class ISqlFKMatchModel; + +class IMatchModelFactory : public QObject +{ + Q_OBJECT +public: + explicit IMatchModelFactory(QObject *parent = nullptr); + + virtual ISqlFKMatchModel *createModel() = 0; +}; + +#endif // IMATCHMODELFACTORY_H diff --git a/src/utils/sqldelegate/isqlfkmatchmodel.cpp b/src/utils/sqldelegate/isqlfkmatchmodel.cpp new file mode 100644 index 0000000..5e7f504 --- /dev/null +++ b/src/utils/sqldelegate/isqlfkmatchmodel.cpp @@ -0,0 +1,58 @@ +#include "isqlfkmatchmodel.h" + +#include + +const QString ISqlFKMatchModel::ellipsesString = QStringLiteral("..."); + +ISqlFKMatchModel::ISqlFKMatchModel(QObject *parent) : + QAbstractTableModel(parent), + hasEmptyRow(true), + size(0) +{ + +} + +int ISqlFKMatchModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : size; +} + +int ISqlFKMatchModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : 1; +} + +void ISqlFKMatchModel::refreshData() +{ + +} + +void ISqlFKMatchModel::clearCache() +{ + +} + +QString ISqlFKMatchModel::getName(db_id /*id*/) const +{ + return QString(); +} + +QFont initBoldFont() +{ + QFont f; + f.setBold(true); + f.setItalic(true); + return f; +} + +//static +QVariant ISqlFKMatchModel::boldFont() +{ + static QFont emptyRowF = initBoldFont(); + return emptyRowF; +} + +void ISqlFKMatchModel::setHasEmptyRow(bool value) +{ + hasEmptyRow = value; +} diff --git a/src/utils/sqldelegate/isqlfkmatchmodel.h b/src/utils/sqldelegate/isqlfkmatchmodel.h new file mode 100644 index 0000000..5ccd7d9 --- /dev/null +++ b/src/utils/sqldelegate/isqlfkmatchmodel.h @@ -0,0 +1,61 @@ +#ifndef ISQLFKMATCHMODEL_H +#define ISQLFKMATCHMODEL_H + +#include +#include "utils/types.h" + +#define MaxMatchItems 10 //TODO: use constexpr + +class ISqlFKMatchModel : public QAbstractTableModel +{ + Q_OBJECT +public: + explicit ISqlFKMatchModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + virtual void autoSuggest(const QString& text) = 0; + virtual void refreshData(); + virtual void clearCache(); + + virtual QString getName(db_id id) const; + + inline bool isEmptyRow(int row) const + { + //If it's last row and there isn't '...' or if it's just before '...' + return hasEmptyRow && (row == EllipsesRow - 1 || (size < MaxMatchItems + 2 && row == size - 1)); + } + + inline bool isEllipsesRow(int row) const + { + //If there isn't 'Empty' row, ellipses come 1 row before EllipsesRow + return row == (EllipsesRow - (hasEmptyRow ? 0 : 1)); + } + + virtual db_id getIdAtRow(int row) const = 0; + virtual QString getNameAtRow(int row) const = 0; + + void setHasEmptyRow(bool value); + +signals: + void resultsReady(bool forceFirst); + +protected: + static QVariant boldFont(); + const static QString ellipsesString; + +protected: + bool hasEmptyRow; + +public: + enum { + EllipsesRow = MaxMatchItems + 1 //(10 items + Empty + ...) + //Items: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + //Empty: 10 + //Ellipses: 11 + }; + int size; +}; + +#endif // ISQLFKMATCHMODEL_H diff --git a/src/utils/sqldelegate/modelpageswitcher.cpp b/src/utils/sqldelegate/modelpageswitcher.cpp new file mode 100644 index 0000000..36bd11b --- /dev/null +++ b/src/utils/sqldelegate/modelpageswitcher.cpp @@ -0,0 +1,173 @@ +#include "modelpageswitcher.h" + +#include "pageditemmodel.h" + +#include +#include +#include +#include + +ModelPageSwitcher::ModelPageSwitcher(bool autoHide, QWidget *parent) : + QWidget(parent), + model(nullptr), + statusLabel(nullptr), + goToPageBut(nullptr), + refreshModelBut(nullptr), + autoHideMode(autoHide) +{ + QHBoxLayout *lay = new QHBoxLayout(this); + + prevBut = new QPushButton(autoHideMode ? QChar('<') : tr("Prev")); + nextBut = new QPushButton(autoHideMode ? QChar('>') : tr("Next")); + + //NOTE: use released() instead of clicked() and QueuedConnection because prevBut and nextBut might get disabled + // after switching the page and there is an artifact if it is disabled while being clicked so defer disabling + connect(prevBut, &QPushButton::released, this, &ModelPageSwitcher::prevPage, Qt::QueuedConnection); + connect(nextBut, &QPushButton::released, this, &ModelPageSwitcher::nextPage, Qt::QueuedConnection); + + pageSpin = new QSpinBox; + pageSpin->setMinimum(1); + pageSpin->setKeyboardTracking(false); + + if(autoHideMode) + { + connect(pageSpin, static_cast(&QSpinBox::valueChanged), this, &ModelPageSwitcher::goToPage); + } + else + { + statusLabel = new QLabel; + statusLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + goToPageBut = new QPushButton(tr("Go")); + refreshModelBut = new QPushButton(tr("Refresh")); + + connect(goToPageBut, &QPushButton::clicked, this, &ModelPageSwitcher::goToPage); + connect(refreshModelBut, &QPushButton::clicked, this, &ModelPageSwitcher::refreshModel); + } + + //Layout + if(autoHideMode) + { + lay->addWidget(prevBut); + lay->addWidget(pageSpin); + lay->addWidget(nextBut); + lay->setAlignment(Qt::AlignRight); + } + else + { + lay->addWidget(statusLabel); + lay->addWidget(prevBut); + lay->addWidget(nextBut); + lay->addWidget(pageSpin); + lay->addWidget(goToPageBut); + lay->addWidget(refreshModelBut); + } +} + +void ModelPageSwitcher::setModel(IPagedItemModel *m) +{ + if(model) + { + disconnect(model, &IPagedItemModel::totalItemsCountChanged, this, &ModelPageSwitcher::totalItemCountChanged); + disconnect(model, &IPagedItemModel::pageCountChanged, this, &ModelPageSwitcher::pageCountChanged); + disconnect(model, &IPagedItemModel::currentPageChanged, this, &ModelPageSwitcher::currentPageChanged); + disconnect(model, &IPagedItemModel::destroyed, this, &ModelPageSwitcher::onModelDestroyed); + } + + model = m; + + if(model) + { + connect(model, &IPagedItemModel::totalItemsCountChanged, this, &ModelPageSwitcher::totalItemCountChanged); + connect(model, &IPagedItemModel::pageCountChanged, this, &ModelPageSwitcher::pageCountChanged); + connect(model, &IPagedItemModel::currentPageChanged, this, &ModelPageSwitcher::currentPageChanged); + connect(model, &IPagedItemModel::destroyed, this, &ModelPageSwitcher::onModelDestroyed); + + recalcPages(); + } +} + +void ModelPageSwitcher::onModelDestroyed() +{ + model = nullptr; +} + +void ModelPageSwitcher::totalItemCountChanged() +{ + recalcText(); +} + +void ModelPageSwitcher::pageCountChanged() +{ + recalcPages(); +} + +void ModelPageSwitcher::currentPageChanged() +{ + recalcPages(); +} + +void ModelPageSwitcher::recalcText() +{ + if(!statusLabel) + return; + + statusLabel->setText(tr("Page %1/%2 (%3 items)") + .arg(model->currentPage() + 1) + .arg(model->getPageCount()) + .arg(model->getTotalItemsCount())); +} + +void ModelPageSwitcher::recalcPages() +{ + int curPage = model->currentPage(); + int count = model->getPageCount(); + if(count == 0) + count = 1; + + pageSpin->setMaximum(count); + prevBut->setEnabled(curPage > 0); + nextBut->setEnabled(curPage < count - 1); + + //NOTE: redraw to avoid artifact like disabled but still highlighted + prevBut->update(); + nextBut->update(); + + if(autoHideMode) + { + pageSpin->setValue(curPage + 1); + + if(count == 1) + hide(); + else + show(); + } + + recalcText(); +} + +void ModelPageSwitcher::prevPage() +{ + if(model) + model->switchToPage(model->currentPage() - 1); +} + +void ModelPageSwitcher::nextPage() +{ + if(model) + model->switchToPage(model->currentPage() + 1); +} + +void ModelPageSwitcher::goToPage() +{ + if(model) + model->switchToPage(pageSpin->value() - 1); +} + +void ModelPageSwitcher::refreshModel() +{ + if(model) + { + model->refreshData(); + model->clearCache(); + } +} diff --git a/src/utils/sqldelegate/modelpageswitcher.h b/src/utils/sqldelegate/modelpageswitcher.h new file mode 100644 index 0000000..92153d1 --- /dev/null +++ b/src/utils/sqldelegate/modelpageswitcher.h @@ -0,0 +1,48 @@ +#ifndef MODELPAGESWITCHER_H +#define MODELPAGESWITCHER_H + +#include + +class QLabel; +class QPushButton; +class QSpinBox; + +class IPagedItemModel; + +class ModelPageSwitcher : public QWidget +{ + Q_OBJECT +public: + ModelPageSwitcher(bool autoHide, QWidget *parent = nullptr); + + void setModel(IPagedItemModel *m); + +public slots: + void prevPage(); + void nextPage(); + +private slots: + void onModelDestroyed(); + void totalItemCountChanged(); + void pageCountChanged(); + void currentPageChanged(); + + void goToPage(); + void refreshModel(); + +private: + void recalcText(); + void recalcPages(); + +private: + IPagedItemModel *model; + QLabel *statusLabel; + QPushButton *prevBut; + QPushButton *nextBut; + QSpinBox *pageSpin; + QPushButton *goToPageBut; + QPushButton *refreshModelBut; + bool autoHideMode; +}; + +#endif // MODELPAGESWITCHER_H diff --git a/src/utils/sqldelegate/pageditemmodel.cpp b/src/utils/sqldelegate/pageditemmodel.cpp new file mode 100644 index 0000000..3fb0d11 --- /dev/null +++ b/src/utils/sqldelegate/pageditemmodel.cpp @@ -0,0 +1,57 @@ +#include "pageditemmodel.h" + +IPagedItemModel::IPagedItemModel(const int itemsPerPage, sqlite3pp::database &db, QObject *parent) : + QAbstractTableModel(parent), + mDb(db), + totalItemsCount(0), + curItemCount(0), + pageCount(0), + curPage(0), + ItemsPerPage(itemsPerPage) +{ + +} + +int IPagedItemModel::getSortingColumn() const +{ + return sortColumn; +} + +qint64 IPagedItemModel::getTotalItemsCount() +{ + return totalItemsCount; +} + +int IPagedItemModel::getPageCount() +{ + return pageCount; +} + +int IPagedItemModel::currentPage() +{ + return curPage; +} + +void IPagedItemModel::switchToPage(int page) +{ + if(curPage == page || page < 0 || page >= pageCount) + return; + + clearCache(); + curPage = page; + + const int rem = totalItemsCount % ItemsPerPage; + const int items = (curPage == pageCount - 1 && rem) ? rem : ItemsPerPage; + if(items != curItemCount) + { + beginResetModel(); + curItemCount = items; + endResetModel(); + } + + emit currentPageChanged(curPage); + + QModelIndex first = index(0, 0); + QModelIndex last = index(curItemCount - 1, columnCount() - 1); + emit dataChanged(first, last); +} diff --git a/src/utils/sqldelegate/pageditemmodel.h b/src/utils/sqldelegate/pageditemmodel.h new file mode 100644 index 0000000..3b4bffc --- /dev/null +++ b/src/utils/sqldelegate/pageditemmodel.h @@ -0,0 +1,61 @@ +#ifndef PAGEDITEMMODEL_H +#define PAGEDITEMMODEL_H + +#include + +#include "utils/types.h" + +namespace sqlite3pp { +class database; +} + +class IPagedItemModel : public QAbstractTableModel +{ + Q_OBJECT +public: + /* Full index: represents the index which the item would have in the table ordered by sorting rules + * Local index: is the row of the item inside the page (valid only for the page of the item) + */ + + + IPagedItemModel(const int itemsPerPage, sqlite3pp::database &db, QObject *parent = nullptr); + + inline sqlite3pp::database &getDb() const { return mDb; } + + // Cached rows management + virtual void clearCache() = 0; + virtual void refreshData() = 0; + + // Sorting TODO: enable multiple columns sort/filter with custom QHeaderView + virtual void setSortingColumn(int col) = 0; + int getSortingColumn() const; + + // Items + qint64 getTotalItemsCount(); + + // Pages + int getPageCount(); + int currentPage(); + void switchToPage(int page); + +signals: + void modelError(const QString& msg); + + // Items signals + void totalItemsCountChanged(qint64 count); + void itemsReady(int startRow, int endRow); //In local indices + + // Page signals + void pageCountChanged(int count); + void currentPageChanged(int page); + +protected: + sqlite3pp::database &mDb; + qint64 totalItemsCount; + int curItemCount; + int pageCount; + int curPage; + int sortColumn; + const int ItemsPerPage; +}; +#endif // PAGEDITEMMODEL_H diff --git a/src/utils/sqldelegate/sqlfkfielddelegate.cpp b/src/utils/sqldelegate/sqlfkfielddelegate.cpp new file mode 100644 index 0000000..390d303 --- /dev/null +++ b/src/utils/sqldelegate/sqlfkfielddelegate.cpp @@ -0,0 +1,51 @@ +#include "sqlfkfielddelegate.h" + +#include "customcompletionlineedit.h" + +#include "imatchmodelfactory.h" +#include "isqlfkmatchmodel.h" +#include "IFKField.h" + +SqlFKFieldDelegate::SqlFKFieldDelegate(IMatchModelFactory *factory, IFKField *iface, QObject *parent) : + QStyledItemDelegate(parent), + mIface(iface), + mFactory(factory) +{ + +} + +QWidget *SqlFKFieldDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &/*option*/, const QModelIndex &/*index*/) const +{ + ISqlFKMatchModel *model = mFactory->createModel(); + CustomCompletionLineEdit *editor = new CustomCompletionLineEdit(model, parent); + model->setParent(editor); + connect(editor, &CustomCompletionLineEdit::completionDone, this, &SqlFKFieldDelegate::handleCompletionDone); + return editor; +} + +void SqlFKFieldDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + CustomCompletionLineEdit *ed = static_cast(editor); + db_id dataId = 0; + QString name; + if(mIface->getFieldData(index.row(), index.column(), dataId, name)) + { + ed->setData(dataId, name); + } +} + +void SqlFKFieldDelegate::setModelData(QWidget *editor, QAbstractItemModel */*model*/, const QModelIndex &index) const +{ + CustomCompletionLineEdit *ed = static_cast(editor); + //FIXME: use also validateData() ? + db_id dataId = 0; + QString name; + ed->getData(dataId, name); + mIface->setFieldData(index.row(), index.column(), dataId, name); +} + +void SqlFKFieldDelegate::handleCompletionDone(CustomCompletionLineEdit *editor) +{ + commitData(editor); + closeEditor(editor); +} diff --git a/src/utils/sqldelegate/sqlfkfielddelegate.h b/src/utils/sqldelegate/sqlfkfielddelegate.h new file mode 100644 index 0000000..4e543a9 --- /dev/null +++ b/src/utils/sqldelegate/sqlfkfielddelegate.h @@ -0,0 +1,30 @@ +#ifndef SQLFKFIELDDELEGATE_H +#define SQLFKFIELDDELEGATE_H + +#include + +class IFKField; +class IMatchModelFactory; +class CustomCompletionLineEdit; + +class SqlFKFieldDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + SqlFKFieldDelegate(IMatchModelFactory *factory, IFKField *iface, QObject *parent = nullptr); + + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override; + +private slots: + void handleCompletionDone(CustomCompletionLineEdit *editor); + +private: + IFKField *mIface; + IMatchModelFactory *mFactory; +}; + +#endif // SQLFKFIELDDELEGATE_H diff --git a/src/utils/thread/CMakeLists.txt b/src/utils/thread/CMakeLists.txt new file mode 100644 index 0000000..3790ac2 --- /dev/null +++ b/src/utils/thread/CMakeLists.txt @@ -0,0 +1,7 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + utils/thread/iquittabletask.h + + utils/thread/iquittabletask.cpp + PARENT_SCOPE +) diff --git a/src/utils/thread/iquittabletask.cpp b/src/utils/thread/iquittabletask.cpp new file mode 100644 index 0000000..2d7a31b --- /dev/null +++ b/src/utils/thread/iquittabletask.cpp @@ -0,0 +1,63 @@ +#include "iquittabletask.h" + +#include + +IQuittableTask::IQuittableTask(QObject *receiver) : + QRunnable(), + mReceiver(receiver), + mPointerGuard(false), + mQuit(false) +{ + setAutoDelete(false); +} + +void IQuittableTask::stop() +{ +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + mQuit.storeRelaxed(true); +#else + mQuit.store(true); +#endif +} + +void IQuittableTask::cleanup() +{ + while (!mPointerGuard.testAndSetOrdered(false, true)) + { + //Busy wait + } + + if(mReceiver) + { + mReceiver = nullptr; + mPointerGuard.testAndSetOrdered(true, false); + } + else + { + delete this; + } +} + +void IQuittableTask::sendEvent(QEvent *e, bool finish) +{ + while (!mPointerGuard.testAndSetOrdered(false, true)) + { + //Busy wait + } + + if(mReceiver) + { + qApp->postEvent(mReceiver, e); + if(finish) + mReceiver = nullptr; + mPointerGuard.testAndSetOrdered(true, false); + } + else + { + delete e; + if(finish) + delete this; + else + mPointerGuard.testAndSetOrdered(true, false); + } +} diff --git a/src/utils/thread/iquittabletask.h b/src/utils/thread/iquittabletask.h new file mode 100644 index 0000000..e80c07d --- /dev/null +++ b/src/utils/thread/iquittabletask.h @@ -0,0 +1,49 @@ +#ifndef IQUITTABLETASK_H +#define IQUITTABLETASK_H + +#include +#include + +class QObject; +class QEvent; + +class IQuittableTask : public QRunnable +{ +public: + IQuittableTask(QObject *receiver); + + void stop(); + void cleanup(); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + inline bool wasStopped() const { return mQuit.loadRelaxed(); } +#else + inline bool wasStopped() const { return mQuit.load(); } +#endif + + /* NOTE: use carefully and only when not running + * + * This is particulary useful if you want to re-use + * an already existend task that has already finished running + * + * Because when finishing a task would call 'sendEvent(event, true)' + * which will set mReceiver to nullptr to allow deleting un-tracked tasks + * (when aborting tasks for example we call 'stop()' and instead of waiting we clear the pointer + * holding the task, so to avoid memory leak the task, which knows it has been aborted, deletes himself) + * + * So when we re-use the task we must set mReceiver again + */ + //NOTE: this i useful to re-use an existent task after it finished because mReceiver is set to nullptr in + inline void setReceiver(QObject *obj) { mReceiver = obj; } + +protected: + void sendEvent(QEvent *e, bool finish); + inline QAtomicInteger* getQuit() { return &mQuit; } + +private: + QObject *mReceiver; + QAtomicInteger mPointerGuard; + QAtomicInteger mQuit; +}; + +#endif // IQUITTABLETASK_H diff --git a/src/utils/types.h b/src/utils/types.h new file mode 100644 index 0000000..01ff4da --- /dev/null +++ b/src/utils/types.h @@ -0,0 +1,72 @@ +#ifndef TYPES_H +#define TYPES_H + +#include + +//64 bit signed integer used for SQL Primary Key ID +typedef qint64 db_id; + +enum class RsType : qint8 +{ + Engine = 0, + FreightWagon, + Coach , + NTypes +}; + +enum class RsEngineSubType : qint8 +{ + Invalid = 0, + Electric, + Diesel, + Steam, + NTypes +}; + +typedef enum { + ToggleType = -1, //Used as flag in StopModel::setStopTypeRange() + Normal = 0, + Transit, + TransitLineChange, + First, + Last +} StopType; + +typedef enum { + Uncoupled = 0, + Coupled = 1 +} RsOp; + +enum class LineType +{ + NonElectric = 0, + Electric = 1 +}; + +enum class JobCategory : qint8 +{ + FREIGHT = 0, + LIS, //Locomotiva In Spostamento (Rimando) + POSTAL, + + REGIONAL, //Passenger + FAST_REGIONAL, //RV - Regionale veloce + LOCAL, + INTERCITY, + EXPRESS, + DIRECT, + HIGH_SPEED, + + NCategories +}; + +constexpr JobCategory LastFreightCategory = JobCategory::POSTAL; +constexpr JobCategory FirstPassengerCategory = JobCategory::REGIONAL; + +typedef struct JobEntry_ +{ + db_id jobId; + JobCategory category; +} JobEntry; + +#endif // TYPES_H diff --git a/src/utils/worker_event_types.h b/src/utils/worker_event_types.h new file mode 100644 index 0000000..a94dbca --- /dev/null +++ b/src/utils/worker_event_types.h @@ -0,0 +1,44 @@ +#ifndef WORKER_EVENT_TYPES_H +#define WORKER_EVENT_TYPES_H + +#include + +//Here we define custom QEvent types in a central place to avoid conflicts + +enum class CustomEvents +{ + //Searchbox + SearchBoxResults = QEvent::User + 1, + + //RS error checker + RsErrWorkerProgress, + RsErrWorkerResult, + + // RS Import + RsImportLoadProgress, + RsImportGoBackPrevPage, + RsImportedRSModelResult, + RsImportedModelsResult, + RsImportedOwnersResult, + RsImportCheckDuplicates, + + //RS main models + RollingstockModelResult, + RsModelsModelResult, + RsOwnersModelResult, + RsOnDemandListModelResult, + + //Shift + ShiftWorkerResult, + + //Stations + StationsModelResult, + StationLinesListModelResult, + LinesModelResult, + RailwayNodeModelResult, + + //Jobs + JobsModelResult +}; + +#endif // WORKER_EVENT_TYPES_H diff --git a/src/viewmanager/CMakeLists.txt b/src/viewmanager/CMakeLists.txt new file mode 100644 index 0000000..91eb24d --- /dev/null +++ b/src/viewmanager/CMakeLists.txt @@ -0,0 +1,19 @@ +set(TRAINTIMETABLE_SOURCES + ${TRAINTIMETABLE_SOURCES} + viewmanager/rsjobviewer.h + viewmanager/rsplanmodel.h + viewmanager/sessionstartendmodel.h + viewmanager/sessionstartendrsviewer.h + viewmanager/treeitem.h + viewmanager/treemodel.h + viewmanager/viewmanager.h + + viewmanager/rsjobviewer.cpp + viewmanager/rsplanmodel.cpp + viewmanager/sessionstartendmodel.cpp + viewmanager/sessionstartendrsviewer.cpp + viewmanager/treeitem.cpp + viewmanager/treemodel.cpp + viewmanager/viewmanager.cpp + PARENT_SCOPE +) diff --git a/src/viewmanager/rsjobviewer.cpp b/src/viewmanager/rsjobviewer.cpp new file mode 100644 index 0000000..2053838 --- /dev/null +++ b/src/viewmanager/rsjobviewer.cpp @@ -0,0 +1,130 @@ +#include "rsjobviewer.h" + +#include +#include +#include +#include +#include +#include + +#include "app/session.h" +#include "app/scopedebug.h" + +#include "utils/rs_utils.h" +#include "utils/rs_types_names.h" + +#include "rsplanmodel.h" + +#include "viewmanager/viewmanager.h" + +#include + +RSJobViewer::RSJobViewer(QWidget *parent) : + QWidget(parent) +{ + QVBoxLayout *lay = new QVBoxLayout(this); + + infoLabel = new QLabel(this); + lay->addWidget(infoLabel); + + view = new QTableView(this); + model = new RsPlanModel(Session->m_Db, this); + view->setModel(model); + + view->setContextMenuPolicy(Qt::CustomContextMenu); + connect(view, &QTableView::customContextMenuRequested, this, &RSJobViewer::showContextMenu); + + QPushButton *updateBut = new QPushButton(tr("Update"), this); + lay->addWidget(updateBut); + lay->addWidget(view); + + connect(updateBut, &QPushButton::clicked, this, &RSJobViewer::updateInfo); + + setMinimumSize(350, 400); +} + +RSJobViewer::~RSJobViewer() +{ + +} + +void RSJobViewer::setRS(db_id id) +{ + m_rsId = id; +} + +void RSJobViewer::updatePlan() +{ + //TODO: clear data 5 seconds after window is hidden + model->loadPlan(m_rsId); + view->resizeColumnsToContents(); +} + +void RSJobViewer::showContextMenu(const QPoint &pos) +{ + QModelIndex idx = view->indexAt(pos); + if(!idx.isValid()) + return; + + RsPlanItem item = model->getItem(idx.row()); + + QMenu menu(this); + + QAction *showInJobEditor = new QAction(tr("Show in Job Editor"), &menu); + + menu.addAction(showInJobEditor); + + QAction *act = menu.exec(view->viewport()->mapToGlobal(pos)); + if(act == showInJobEditor) + { + Session->getViewManager()->requestJobEditor(item.jobId, item.stopId); + } +} + +void RSJobViewer::updateRsInfo() +{ + query q_getRSInfo(Session->m_Db, "SELECT rs_list.number,rs_models.name,rs_models.suffix,rs_models.type,rs_owners.name" + " FROM rs_list" + " LEFT JOIN rs_models ON rs_models.id=rs_list.model_id" + " LEFT JOIN rs_owners ON rs_owners.id=rs_list.owner_id" + " WHERE rs_list.id=?"); + q_getRSInfo.bind(1, m_rsId); + int ret = q_getRSInfo.step(); + if(ret != SQLITE_ROW) + { + //Error: RS does not exist! + close(); + return; + } + + auto rs = q_getRSInfo.getRows(); + sqlite3_stmt *stmt = q_getRSInfo.stmt(); + int number = rs.get(0); + int modelNameLen = sqlite3_column_bytes(stmt, 1); + const char *modelName = reinterpret_cast(sqlite3_column_text(stmt, 1)); + + int modelSuffixLen = sqlite3_column_bytes(stmt, 2); + const char *modelSuffix = reinterpret_cast(sqlite3_column_text(stmt, 2)); + RsType type = RsType(rs.get(3)); + + + int ownerLen = sqlite3_column_bytes(stmt, 4); + const char *owner = reinterpret_cast(sqlite3_column_text(stmt, 4)); + + const QString name = rs_utils::formatNameRef(modelName, modelNameLen, + number, + modelSuffix, modelSuffixLen, + type); + setWindowTitle(name); + + const QString info = tr("Type: %1
" + "Owner: %2").arg(RsTypeNames::name(type)); + + infoLabel->setText(owner ? info.arg(QLatin1String(owner, ownerLen)) : info.arg(tr("Not set!"))); +} + +void RSJobViewer::updateInfo() +{ + updateRsInfo(); + updatePlan(); +} diff --git a/src/viewmanager/rsjobviewer.h b/src/viewmanager/rsjobviewer.h new file mode 100644 index 0000000..3d38b98 --- /dev/null +++ b/src/viewmanager/rsjobviewer.h @@ -0,0 +1,41 @@ +#ifndef RSJOBVIEWER_H +#define RSJOBVIEWER_H + +#include + +#include "utils/types.h" + +class QTimeEdit; +class QTableView; +class QLabel; +class RsPlanModel; + +class RSJobViewer : public QWidget +{ + Q_OBJECT +public: + RSJobViewer(QWidget *parent = nullptr); + virtual ~RSJobViewer(); + + void setRS(db_id id); + db_id m_rsId; + + void updatePlan(); + void updateRsInfo(); + +public slots: + void updateInfo(); + +private slots: + void showContextMenu(const QPoint &pos); + +private: + QTableView *view; + RsPlanModel *model; + + QTimeEdit *timeEdit1; + QTimeEdit *timeEdit2; + QLabel *infoLabel; +}; + +#endif // RSJOBVIEWER_H diff --git a/src/viewmanager/rsplanmodel.cpp b/src/viewmanager/rsplanmodel.cpp new file mode 100644 index 0000000..4037993 --- /dev/null +++ b/src/viewmanager/rsplanmodel.cpp @@ -0,0 +1,127 @@ +#include "rsplanmodel.h" + +#include "utils/jobcategorystrings.h" + +#include +using namespace sqlite3pp; + +RsPlanModel::RsPlanModel(sqlite3pp::database &db, + QObject *parent) : + QAbstractTableModel(parent), + mDb(db) +{ + +} + +QVariant RsPlanModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch (section) + { + case JobName: + return tr("Job"); + case Station: + return tr("Station"); + case Arrival: + return tr("Arrival"); + case Departure: + return tr("Departure"); + case Operation: + return tr("Operation"); + default: + break; + } + } + return QAbstractTableModel::headerData(section, orientation, role); +} + +int RsPlanModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_data.size(); +} + +int RsPlanModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : NCols; +} + +QVariant RsPlanModel::data(const QModelIndex &idx, int role) const +{ + if (!idx.isValid() || idx.row() >= m_data.size() || idx.column() >= NCols || role != Qt::DisplayRole) + return QVariant(); + + const RsPlanItem& item = m_data.at(idx.row()); + switch (idx.column()) + { + case JobName: + return JobCategoryName::jobName(item.jobId, item.jobCat); + case Station: + return item.stationName; + case Arrival: + return item.arrival; + case Departure: + return item.departure; + case Operation: + return item.op == RsOp::Coupled ? + tr("Coupled") : tr("Uncoupled"); + } + + return QVariant(); +} + +void RsPlanModel::loadPlan(db_id rsId) +{ + beginResetModel(); + + m_data.clear(); + + //TODO: load in thread with same query prepared form many models + query q_selectOps(mDb, + "SELECT stops.id," + "stops.jobId," + "jobs.category," + "stops.stationId," + "stops.arrival," + "stops.departure," + "coupling.operation," + "stations.name" + " FROM stops" + " JOIN coupling ON coupling.stopId=stops.id AND coupling.rsId=?" + " JOIN jobs ON jobs.id=stops.jobId" + " JOIN stations ON stations.id=stops.stationId" + " ORDER BY stops.arrival"); + + q_selectOps.bind(1, rsId); + for(auto r : q_selectOps) + { + RsPlanItem item; + item.stopId = r.get(0); + item.jobId = r.get(1); + item.jobCat = JobCategory(r.get(2)); + item.stationId = r.get(3); + item.arrival = r.get(4); + item.departure = r.get(5); + item.op = RsOp(r.get(6)); + item.stationName = r.get(7); + + m_data.append(item); + } + + m_data.squeeze(); + + endResetModel(); +} + +void RsPlanModel::clear() +{ + beginResetModel(); + m_data.clear(); + m_data.squeeze(); + endResetModel(); +} + +RsPlanItem RsPlanModel::getItem(int row) +{ + return m_data.value(row, RsPlanItem()); +} diff --git a/src/viewmanager/rsplanmodel.h b/src/viewmanager/rsplanmodel.h new file mode 100644 index 0000000..27b4679 --- /dev/null +++ b/src/viewmanager/rsplanmodel.h @@ -0,0 +1,66 @@ +#ifndef RSPLANMODEL_H +#define RSPLANMODEL_H + +#include + +#include + +#include + +#include "utils/types.h" + +namespace sqlite3pp { +class database; +} + +typedef struct RsPlanItem_ +{ + db_id jobId; + db_id stopId; + db_id stationId; + QTime arrival; + QTime departure; + QString stationName; + JobCategory jobCat; + RsOp op; +} RsPlanItem; + +class RsPlanModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + enum Columns + { + JobName = 0, + Station, + Arrival, + Departure, + Operation, + NCols + }; + + RsPlanModel(sqlite3pp::database& db, + QObject *parent = nullptr); + + // 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; + + void loadPlan(db_id rsId); + void clear(); + + RsPlanItem getItem(int row); + +private: + sqlite3pp::database& mDb; + + QVector m_data; +}; + +#endif // RSPLANMODEL_H diff --git a/src/viewmanager/sessionstartendmodel.cpp b/src/viewmanager/sessionstartendmodel.cpp new file mode 100644 index 0000000..a102845 --- /dev/null +++ b/src/viewmanager/sessionstartendmodel.cpp @@ -0,0 +1,334 @@ +#include "sessionstartendmodel.h" + +#include + +#include + +#include "utils/jobcategorystrings.h" + +#include "utils/platform_utils.h" +#include "utils/rs_utils.h" + +#include + +static constexpr quintptr PARENT = quintptr(-1); + +SessionStartEndModel::SessionStartEndModel(sqlite3pp::database &db, QObject *parent) : + QAbstractItemModel(parent), + mDb(db), + m_mode(SessionRSMode::StartOfSession), + m_order(SessionRSOrder::ByStation) +{ + +} + +QVariant SessionStartEndModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch (section) + { + case RsNameCol: + return tr("Rollingstock"); + case JobCol: + return tr("Job"); + case PlatfCol: + return tr("Platform"); + case DepartureCol: + return m_mode == SessionRSMode::StartOfSession ? tr("Departure") : tr("Arrival"); + case StationOrOwnerCol: + return m_order == SessionRSOrder::ByStation ? tr("Owner") : tr("Station"); //Opposite of order + default: + break; + } + } + return QAbstractItemModel::headerData(section, orientation, role); +} + +QModelIndex SessionStartEndModel::index(int row, int column, const QModelIndex &p) const +{ + if(p.isValid()) + { + if(p.internalId() != PARENT) + return QModelIndex(); + + //It's a rs + int parentRow = p.row(); + if(parentRow < 0 || parentRow >= parents.size()) + return QModelIndex(); + + if(row < 0 || (row + parents[parentRow].firstIdx) >= rsData.size() || column < 0 || column >= NCols) + return QModelIndex(); + + QModelIndex par = parent(createIndex(row, column, parentRow)); + if(par != p) + { + qWarning() << "IDX ERROR" << p << par; + par = parent(createIndex(row, column, parentRow)); + } + + return createIndex(row, column, parentRow); + } + + //Station or RS owner + if(row < 0 || row >= parents.size() || column != 0) + return QModelIndex(); + + Q_ASSERT(parent(createIndex(row, column, PARENT)) == p); + + return createIndex(row, column, PARENT); +} + +QModelIndex SessionStartEndModel::parent(const QModelIndex &idx) const +{ + if(!idx.isValid() || idx.internalId() == PARENT) + return QModelIndex(); + + int parentRow = int(idx.internalId()); + + return createIndex(parentRow, 0, PARENT); +} + +int SessionStartEndModel::rowCount(const QModelIndex &p) const +{ + if(p.isValid()) + { + if(p.internalId() != PARENT) + return 0; //RS + + //Station or Owner + int firstIdx = parents.at(p.row()).firstIdx; + int lastIdx = rsData.size(); //Until end + if(p.row() + 1 < parents.size()) + lastIdx = parents.at(p.row() + 1).firstIdx; //Until next station/owner first index + return lastIdx - firstIdx; + } + + //Root + return parents.size(); +} + +int SessionStartEndModel::columnCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 1 : NCols; +} + +QVariant SessionStartEndModel::data(const QModelIndex &idx, int role) const +{ + if(!idx.isValid()) + return QVariant(); + + if(idx.internalId() == PARENT) + { + //Station or Owner + switch (role) + { + case Qt::DisplayRole: + { + return parents.at(idx.row()).name; + } + case Qt::FontRole: + { + QFont f; + f.setBold(true); + return f; + } + } + } + else + { + //RS + int realIdx = parents.at(int(idx.internalId())).firstIdx + idx.row(); + const RSItem& item = rsData.at(realIdx); + switch (role) + { + case Qt::DisplayRole: + { + switch (idx.column()) + { + case RsNameCol: + return item.rsName; + case JobCol: + //return item.jobId; + return JobCategoryName::jobName(item.jobId, item.jobCategory); + case PlatfCol: + return utils::platformName(item.platform); + case DepartureCol: + return item.time; + case StationOrOwnerCol: + { + return item.stationOrOwnerName; + } + } + } + } + } + + return QVariant(); +} + +bool SessionStartEndModel::hasChildren(const QModelIndex &parent) const +{ + if(parent.isValid()) + { + if(parent.internalId() == PARENT) + return true; + return false; + } + + return parents.size(); +} + +QModelIndex SessionStartEndModel::sibling(int row, int column, const QModelIndex &idx) const +{ + if(!idx.isValid()) + return QModelIndex(); + + if(idx.internalId() == PARENT) + { + if(row < parents.size() && column == 1) + return createIndex(row, column, PARENT); + } + else + { + int parentIdx = int(idx.internalId()); + int firstIdx = parents.at(parentIdx).firstIdx; + int lastIdx = rsData.size(); + if(parentIdx + 1 < parents.size()) + lastIdx = parents.at(parentIdx + 1).firstIdx; + + int count = firstIdx - lastIdx; + if(row < count) + return createIndex(row, column, parentIdx); + } + + return QModelIndex(); +} + +Qt::ItemFlags SessionStartEndModel::flags(const QModelIndex &idx) const +{ + Qt::ItemFlags f; + + if(!idx.isValid()) + return f; + + f.setFlag(Qt::ItemIsEnabled); + f.setFlag(Qt::ItemIsSelectable); + + if(idx.internalId() != PARENT) + f.setFlag(Qt::ItemNeverHasChildren); + + return f; +} + +void SessionStartEndModel::setMode(SessionRSMode m, SessionRSOrder o, bool forceReload) +{ + if(m_mode == m && m_order == o && !forceReload) + return; + + beginResetModel(); + m_mode = m; + m_order = o; + + rsData.clear(); + parents.clear(); + + sqlite3pp::query q(mDb); + + q.prepare("SELECT COUNT(DISTINCT rsId) FROM coupling"); + q.step(); + int count = q.getRows().get(0); + q.reset(); + + rsData.reserve(count); + + //TODO: fetch departure instead of arrival for start session + + //Query template: MIN/MAX to get RS at start/end of session then order by station/s_owner + const auto sql = QStringLiteral("SELECT %1(stops.arrival), stops.stationId, rs_list.owner_id," + " stations.name, rs_owners.name," + " rs_list.id, rs_models.name, rs_models.suffix, rs_list.number, rs_models.type," + " stops.jobId, jobs.category," + " stops.platform, coupling.operation" + " FROM rs_list" + " JOIN coupling ON coupling.rsId=rs_list.id" + " JOIN stops ON stops.id=coupling.stopId" + " JOIN stations ON stations.id=stops.stationId" + " JOIN jobs ON jobs.id=stops.jobId" + " LEFT JOIN rs_models ON rs_models.id=rs_list.model_id" + " LEFT JOIN rs_owners ON rs_owners.id=rs_list.owner_id" //It might be null + " GROUP BY rs_list.id" + " ORDER BY %2, stops.arrival, stops.jobId, rs_list.model_id"); + + QByteArray query = sql.arg(m_mode == SessionRSMode::StartOfSession ? "MIN" : "MAX") + .arg(m_order == SessionRSOrder::ByStation ? "stops.stationId" : "rs_list.owner_id") + .toUtf8(); + + q.prepare(query.constData()); + + db_id lastParentId = 0; + int row = 0; + + sqlite3_stmt *stmt = q.stmt(); + for(auto rs : q) + { + RSItem item; + item.time = rs.get(0); + + db_id stationId = rs.get(1); + db_id ownerId = rs.get(2); + + db_id parentId = 0; + + if(m_order == SessionRSOrder::ByStation) + { + parentId = stationId; + item.stationOrOwnerId = ownerId; + item.stationOrOwnerName = rs.get(4); + }else{ + parentId = ownerId; + item.stationOrOwnerId = stationId; + item.stationOrOwnerName = rs.get(3); + } + + if(parentId != lastParentId) + { + ParentItem s; + s.id = parentId; + s.name = rs.get(m_order == SessionRSOrder::ByStation ? 3 : 4); + s.firstIdx = row; + parents.append(s); + + lastParentId = parentId; + } + + item.parentIdx = parents.size() - 1; + item.rsId = rs.get(5); + + int modelNameLen = sqlite3_column_bytes(stmt, 6); + const char *modelName = reinterpret_cast(sqlite3_column_text(stmt, 6)); + + int modelSuffixLen = sqlite3_column_bytes(stmt, 7); + const char *modelSuffix = reinterpret_cast(sqlite3_column_text(stmt, 7)); + int number = rs.get(8); + RsType type = RsType(rs.get(9)); + + item.rsName = rs_utils::formatNameRef(modelName, modelNameLen, + number, + modelSuffix, modelSuffixLen, + type); + + item.jobId = rs.get(10); + item.jobCategory = JobCategory(rs.get(11)); + + item.platform = rs.get(12); + + //TODO: check operation + + rsData.append(item); + + row++; + } + + endResetModel(); +} diff --git a/src/viewmanager/sessionstartendmodel.h b/src/viewmanager/sessionstartendmodel.h new file mode 100644 index 0000000..e285c24 --- /dev/null +++ b/src/viewmanager/sessionstartendmodel.h @@ -0,0 +1,91 @@ +#ifndef SESSIONSTARTENDMODEL_H +#define SESSIONSTARTENDMODEL_H + +#include + +#include +#include + +#include "utils/types.h" +#include "utils/session_rs_modes.h" + +namespace sqlite3pp { +class database; +} + +//FIXME: make incremental loading, cached like other on-demand models +class SessionStartEndModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + //Station or RS Owner + typedef struct StationOrOwner_ + { + db_id id; + QString name; + int firstIdx; //First RS index + } ParentItem; + + typedef struct RSItem_ + { + db_id rsId; + db_id jobId; + db_id stationOrOwnerId; + int parentIdx; + int platform; + QTime time; + QString rsName; + QString stationOrOwnerName; + JobCategory jobCategory; + } RSItem; + + enum Columns + { + RsNameCol = 0, + JobCol, + PlatfCol, + DepartureCol, + StationOrOwnerCol, + NCols + }; + + + + SessionStartEndModel(sqlite3pp::database &db, QObject *parent = nullptr); + + // Header: + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + // Basic functionality: + QModelIndex index(int row, int column, + const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &idx) const override; + + 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; + + bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; + + QModelIndex sibling(int row, int column, const QModelIndex &idx) const override; + + Qt::ItemFlags flags(const QModelIndex& idx) const override; + + void setMode(SessionRSMode m, SessionRSOrder o, bool forceReload = false); + + inline SessionRSMode mode() const { return m_mode; } + inline SessionRSOrder order() const { return m_order; } + +private: + sqlite3pp::database &mDb; + + QVector parents; + QVector rsData; + + SessionRSMode m_mode; + SessionRSOrder m_order; +}; + +#endif // SESSIONSTARTENDMODEL_H diff --git a/src/viewmanager/sessionstartendrsviewer.cpp b/src/viewmanager/sessionstartendrsviewer.cpp new file mode 100644 index 0000000..7fbe9c1 --- /dev/null +++ b/src/viewmanager/sessionstartendrsviewer.cpp @@ -0,0 +1,100 @@ +#include "sessionstartendrsviewer.h" + +#include "sessionstartendmodel.h" + +#include +#include +#include +#include + +#include +#include + +#include "app/session.h" + +#include "odt_export/sessionrsexport.h" + +#include "utils/file_format_names.h" + +#include + +SessionStartEndRSViewer::SessionStartEndRSViewer(QWidget *parent) : + QWidget(parent) +{ + QVBoxLayout *lay = new QVBoxLayout(this); + + QToolBar *toolBar = new QToolBar; + lay->addWidget(toolBar); + + modeCombo = new QComboBox; + modeCombo->addItem(tr("Show Session Start"), int(SessionRSMode::StartOfSession)); + modeCombo->addItem(tr("Show Session End"), int(SessionRSMode::EndOfSession)); + toolBar->addWidget(modeCombo); + + orderCombo = new QComboBox; + orderCombo->addItem(tr("Order By Station"), int(SessionRSOrder::ByStation)); + orderCombo->addItem(tr("Order By Owner"), int(SessionRSOrder::ByOwner)); + toolBar->addWidget(orderCombo); + + connect(modeCombo, qOverload(&QComboBox::activated), this, &SessionStartEndRSViewer::modeChanged); + connect(orderCombo, qOverload(&QComboBox::activated), this, &SessionStartEndRSViewer::orderChanged); + + toolBar->addAction(tr("Export Sheet"), this, &SessionStartEndRSViewer::exportSheet); + + view = new QTreeView(this); + lay->addWidget(view); + + model = new SessionStartEndModel(Session->m_Db, this); + + view->setModel(model); + view->setSelectionBehavior(QAbstractItemView::SelectRows); + view->setSelectionMode(QAbstractItemView::SingleSelection); + + model->setMode(SessionRSMode::StartOfSession, SessionRSOrder::ByStation, true); + modeCombo->setCurrentIndex(modeCombo->findData(int(model->mode()))); + orderCombo->setCurrentIndex(orderCombo->findData(int(model->order()))); + view->expandAll(); + + setMinimumSize(200, 300); + setWindowTitle(tr("Rollingstock Summary")); +} + +void SessionStartEndRSViewer::orderChanged() +{ + SessionRSOrder order = SessionRSOrder(orderCombo->currentData().toInt()); + + model->setMode(model->mode(), order, false); + view->expandAll(); +} + +void SessionStartEndRSViewer::modeChanged() +{ + SessionRSMode mode = SessionRSMode(modeCombo->currentData().toInt()); + + model->setMode(mode, model->order(), false); + view->expandAll(); +} + +void SessionStartEndRSViewer::exportSheet() +{ + QFileDialog dlg(this, tr("Expoert RS session plan")); + dlg.setFileMode(QFileDialog::AnyFile); + dlg.setAcceptMode(QFileDialog::AcceptSave); + dlg.setDirectory(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); + + 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; + + SessionRSExport w(model->mode(), model->order()); + w.write(); + w.save(fileName); +} diff --git a/src/viewmanager/sessionstartendrsviewer.h b/src/viewmanager/sessionstartendrsviewer.h new file mode 100644 index 0000000..6771b97 --- /dev/null +++ b/src/viewmanager/sessionstartendrsviewer.h @@ -0,0 +1,30 @@ +#ifndef SESSIONSTARTENDRSVIEWER_H +#define SESSIONSTARTENDRSVIEWER_H + +#include + +class QTreeView; +class QComboBox; +class SessionStartEndModel; + +class SessionStartEndRSViewer : public QWidget +{ + Q_OBJECT +public: + explicit SessionStartEndRSViewer(QWidget *parent = nullptr); + +private slots: + void orderChanged(); + void modeChanged(); + + void exportSheet(); +private: + SessionStartEndModel *model; + + QTreeView *view; + + QComboBox *modeCombo; + QComboBox *orderCombo; +}; + +#endif // SESSIONSTARTENDRSVIEWER_H diff --git a/src/viewmanager/treeitem.cpp b/src/viewmanager/treeitem.cpp new file mode 100644 index 0000000..e11639f --- /dev/null +++ b/src/viewmanager/treeitem.cpp @@ -0,0 +1,126 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/* + treeitem.cpp + + A container for items of data supplied by the simple tree model. +*/ + +#include + +#include "treeitem.h" + +//! [0] +TreeItem::TreeItem(const QList &data, TreeItem *parent) +{ + m_parentItem = parent; + m_itemData = data; +} +//! [0] + +//! [1] +TreeItem::~TreeItem() +{ + qDeleteAll(m_childItems); +} +//! [1] + +//! [2] +void TreeItem::appendChild(TreeItem *item) +{ + m_childItems.append(item); +} +//! [2] + +//! [3] +TreeItem *TreeItem::child(int row) +{ + return m_childItems.value(row); +} +//! [3] + +//! [4] +int TreeItem::childCount() const +{ + return m_childItems.count(); +} +//! [4] + +//! [5] +int TreeItem::columnCount() const +{ + return m_itemData.count(); +} +//! [5] + +//! [6] +QVariant TreeItem::data(int column) const +{ + return m_itemData.value(column); +} +//! [6] + +//! [7] +TreeItem *TreeItem::parentItem() +{ + return m_parentItem; +} +//! [7] + +//! [8] +int TreeItem::row() const +{ + if (m_parentItem) + return m_parentItem->m_childItems.indexOf(const_cast(this)); + + return 0; +} +//! [8] diff --git a/src/viewmanager/treeitem.h b/src/viewmanager/treeitem.h new file mode 100644 index 0000000..8d0bb49 --- /dev/null +++ b/src/viewmanager/treeitem.h @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef TREEITEM_H +#define TREEITEM_H + +#include +#include + +//! [0] +class TreeItem +{ +public: + explicit TreeItem(const QList &data, TreeItem *parentItem = 0); + ~TreeItem(); + + void appendChild(TreeItem *child); + + TreeItem *child(int row); + int childCount() const; + int columnCount() const; + QVariant data(int column) const; + int row() const; + TreeItem *parentItem(); + +private: + QList m_childItems; + QList m_itemData; + TreeItem *m_parentItem; +}; +//! [0] + +#endif // TREEITEM_H diff --git a/src/viewmanager/treemodel.cpp b/src/viewmanager/treemodel.cpp new file mode 100644 index 0000000..ab781e9 --- /dev/null +++ b/src/viewmanager/treemodel.cpp @@ -0,0 +1,242 @@ +///**************************************************************************** +//** +//** Copyright (C) 2016 The Qt Company Ltd. +//** Contact: https://www.qt.io/licensing/ +//** +//** This file is part of the examples of the Qt Toolkit. +//** +//** $QT_BEGIN_LICENSE:BSD$ +//** Commercial License Usage +//** Licensees holding valid commercial Qt licenses may use this file in +//** accordance with the commercial license agreement provided with the +//** Software or, alternatively, in accordance with the terms contained in +//** a written agreement between you and The Qt Company. For licensing terms +//** and conditions see https://www.qt.io/terms-conditions. For further +//** information use the contact form at https://www.qt.io/contact-us. +//** +//** BSD License Usage +//** Alternatively, you may use this file under the terms of the BSD license +//** as follows: +//** +//** "Redistribution and use in source and binary forms, with or without +//** modification, are permitted provided that the following conditions are +//** met: +//** * Redistributions of source code must retain the above copyright +//** notice, this list of conditions and the following disclaimer. +//** * Redistributions in binary form must reproduce the above copyright +//** notice, this list of conditions and the following disclaimer in +//** the documentation and/or other materials provided with the +//** distribution. +//** * Neither the name of The Qt Company Ltd nor the names of its +//** contributors may be used to endorse or promote products derived +//** from this software without specific prior written permission. +//** +//** +//** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +//** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +//** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +//** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +//** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +//** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +//** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +//** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +//** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +//** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +//** +//** $QT_END_LICENSE$ +//** +//****************************************************************************/ + +///* +// treemodel.cpp + +// Provides a simple tree model to show how to create and use hierarchical +// models. +//*/ + +////FIXME: remove + +//#include "treeitem.h" +//#include "treemodel.h" + +//#include + +////! [0] +//TreeModel::TreeModel(const QString &data, QObject *parent) +// : QAbstractItemModel(parent) +//{ +// QList rootData; +// rootData << "Title" << "Summary"; +// rootItem = new TreeItem(rootData); +// //setupModelData(data.split(QString("\n")), rootItem); + +// for(int i = 0; i < 4; i++) +// { +// TreeItem *item = new TreeItem({"Figlio:", i, i % 2 == 0 ? "Pari": "Dispari"}, rootItem); +// rootItem->appendChild(item); + +// if(i % 2 == 0) +// { +// TreeItem *sub = new TreeItem({"Ciaone", i, "Colonne", "Bob"}, item); +// item->appendChild(sub); +// } +// } +//} +////! [0] + +////! [1] +//TreeModel::~TreeModel() +//{ +// delete rootItem; +//} +////! [1] + +////! [2] +//int TreeModel::columnCount(const QModelIndex &parent) const +//{ +// if (parent.isValid()) +// return static_cast(parent.internalPointer())->columnCount(); +// else +// return rootItem->columnCount(); +//} +////! [2] + +////! [3] +//QVariant TreeModel::data(const QModelIndex &index, int role) const +//{ +// if (!index.isValid()) +// return QVariant(); + +// if (role != Qt::DisplayRole) +// return QVariant(); + +// TreeItem *item = static_cast(index.internalPointer()); + +// return item->data(index.column()); +//} +////! [3] + +////! [4] +//Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const +//{ +// if (!index.isValid()) +// return 0; + +// return QAbstractItemModel::flags(index); +//} +////! [4] + +////! [5] +//QVariant TreeModel::headerData(int section, Qt::Orientation orientation, +// int role) const +//{ +// if (orientation == Qt::Horizontal && role == Qt::DisplayRole) +// return rootItem->data(section); + +// return QVariant(); +//} +////! [5] + +////! [6] +//QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) +// const +//{ +// if (!hasIndex(row, column, parent)) +// return QModelIndex(); + +// TreeItem *parentItem; + +// if (!parent.isValid()) +// parentItem = rootItem; +// else +// parentItem = static_cast(parent.internalPointer()); + +// TreeItem *childItem = parentItem->child(row); +// if (childItem) +// return createIndex(row, column, childItem); +// else +// return QModelIndex(); +//} +////! [6] + +////! [7] +//QModelIndex TreeModel::parent(const QModelIndex &index) const +//{ +// if (!index.isValid()) +// return QModelIndex(); + +// TreeItem *childItem = static_cast(index.internalPointer()); +// TreeItem *parentItem = childItem->parentItem(); + +// if (parentItem == rootItem) +// return QModelIndex(); + +// return createIndex(parentItem->row(), 0, parentItem); +//} +////! [7] +////WARNING: use this class to spot selection errors in QTreeView, then adapt it +////! [8] +//int TreeModel::rowCount(const QModelIndex &parent) const +//{ +// TreeItem *parentItem; +// if (parent.column() > 0) +// return 0; + +// if (!parent.isValid()) +// parentItem = rootItem; +// else +// parentItem = static_cast(parent.internalPointer()); + +// return parentItem->childCount(); +//} +////! [8] + +//void TreeModel::setupModelData(const QStringList &lines, TreeItem *parent) +//{ +// QList parents; +// QList indentations; +// parents << parent; +// indentations << 0; + +// int number = 0; + +// while (number < lines.count()) { +// int position = 0; +// while (position < lines[number].length()) { +// if (lines[number].at(position) != ' ') +// break; +// position++; +// } + +// QString lineData = lines[number].mid(position).trimmed(); + +// if (!lineData.isEmpty()) { +// // Read the column data from the rest of the line. +// QStringList columnStrings = lineData.split("\t", QString::SkipEmptyParts); +// QList columnData; +// for (int column = 0; column < columnStrings.count(); ++column) +// columnData << columnStrings[column]; + +// if (position > indentations.last()) { +// // The last child of the current parent is now the new parent +// // unless the current parent has no children. + +// if (parents.last()->childCount() > 0) { +// parents << parents.last()->child(parents.last()->childCount()-1); +// indentations << position; +// } +// } else { +// while (position < indentations.last() && parents.count() > 0) { +// parents.pop_back(); +// indentations.pop_back(); +// } +// } + +// // Append a new item to the current parent's list of children. +// parents.last()->appendChild(new TreeItem(columnData, parents.last())); +// } + +// ++number; +// } +//} diff --git a/src/viewmanager/treemodel.h b/src/viewmanager/treemodel.h new file mode 100644 index 0000000..a0ffe60 --- /dev/null +++ b/src/viewmanager/treemodel.h @@ -0,0 +1,86 @@ +///**************************************************************************** +//** +//** Copyright (C) 2016 The Qt Company Ltd. +//** Contact: https://www.qt.io/licensing/ +//** +//** This file is part of the examples of the Qt Toolkit. +//** +//** $QT_BEGIN_LICENSE:BSD$ +//** Commercial License Usage +//** Licensees holding valid commercial Qt licenses may use this file in +//** accordance with the commercial license agreement provided with the +//** Software or, alternatively, in accordance with the terms contained in +//** a written agreement between you and The Qt Company. For licensing terms +//** and conditions see https://www.qt.io/terms-conditions. For further +//** information use the contact form at https://www.qt.io/contact-us. +//** +//** BSD License Usage +//** Alternatively, you may use this file under the terms of the BSD license +//** as follows: +//** +//** "Redistribution and use in source and binary forms, with or without +//** modification, are permitted provided that the following conditions are +//** met: +//** * Redistributions of source code must retain the above copyright +//** notice, this list of conditions and the following disclaimer. +//** * Redistributions in binary form must reproduce the above copyright +//** notice, this list of conditions and the following disclaimer in +//** the documentation and/or other materials provided with the +//** distribution. +//** * Neither the name of The Qt Company Ltd nor the names of its +//** contributors may be used to endorse or promote products derived +//** from this software without specific prior written permission. +//** +//** +//** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +//** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +//** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +//** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +//** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +//** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +//** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +//** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +//** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +//** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +//** +//** $QT_END_LICENSE$ +//** +//****************************************************************************/ + +//#ifndef TREEMODEL_H +//#define TREEMODEL_H + +//#include +//#include +//#include + +//class TreeItem; + +////! [0] +//class TreeModel : public QAbstractItemModel +//{ +// Q_OBJECT + +//public: +// explicit TreeModel(const QString &data, QObject *parent = 0); +// ~TreeModel(); + +// QVariant data(const QModelIndex &index, int role) const override; +// Qt::ItemFlags flags(const QModelIndex &index) const override; +// QVariant headerData(int section, Qt::Orientation orientation, +// int role = Qt::DisplayRole) const override; +// QModelIndex index(int row, int column, +// const QModelIndex &parent = QModelIndex()) const override; +// QModelIndex parent(const QModelIndex &index) const override; +// int rowCount(const QModelIndex &parent = QModelIndex()) const override; +// int columnCount(const QModelIndex &parent = QModelIndex()) const override; + +//private: +// void setupModelData(const QStringList &lines, TreeItem *parent); + +// TreeItem *rootItem; +//}; +////! [0] + +//#endif // TREEMODEL_H diff --git a/src/viewmanager/viewmanager.cpp b/src/viewmanager/viewmanager.cpp new file mode 100644 index 0000000..6427252 --- /dev/null +++ b/src/viewmanager/viewmanager.cpp @@ -0,0 +1,692 @@ +#include "viewmanager.h" + +#include "app/session.h" + +#include "rsjobviewer.h" +#include "rollingstock/manager/rollingstockmanager.h" + +#include "stations/manager/stationsmanager.h" +#include "stations/manager/stationjobview.h" +#include "stations/manager/free_rs_viewer/stationfreersviewer.h" + +#include "lines/linestorage.h" + +#include "shifts/shiftmanager.h" +#include "shifts/shiftviewer.h" +#include "shifts/shiftgraph/shiftgrapheditor.h" + +#include "app/scopedebug.h" + +#include "jobs/jobstorage.h" +#include "jobs/jobeditor/jobpatheditor.h" +#include "jobs/jobsmanager.h" + +#include "graph/graphmanager.h" + +#include "sessionstartendrsviewer.h" + +ViewManager::ViewManager(QObject *parent) : + QObject(parent), + mGraphMgr(nullptr), + m_mainWidget(nullptr), + rsManager(nullptr), + stManager(nullptr), + shiftManager(nullptr), + shiftGraphEditor(nullptr), + jobEditor(nullptr), + jobsManager(nullptr), + sessionRSViewer(nullptr) +{ + mGraphMgr = new GraphManager(this); + + //RollingStock + connect(Session, &MeetingSession::rollingstockRemoved, this, &ViewManager::onRSRemoved); + connect(Session, &MeetingSession::rollingStockPlanChanged, this, &ViewManager::onRSPlanChanged); + connect(Session, &MeetingSession::rollingStockModified, this, &ViewManager::onRSInfoChanged); + + //Stations + LineStorage *lines = Session->mLineStorage; + connect(lines, &LineStorage::stationRemoved, this, &ViewManager::onStRemoved); + connect(lines, &LineStorage::stationNameChanged, this, &ViewManager::onStNameChanged); + connect(lines, &LineStorage::stationModified, this, &ViewManager::onStPlanChanged); + + //Shifts + connect(Session, &MeetingSession::shiftNameChanged, this, &ViewManager::onShiftEdited); + connect(Session, &MeetingSession::shiftRemoved, this, &ViewManager::onShiftRemoved); + connect(Session, &MeetingSession::shiftJobsChanged, this, &ViewManager::onShiftJobsChanged); + + connect(&AppSettings, &TrainTimetableSettings::jobGraphOptionsChanged, this, &ViewManager::onGraphOptionsChanged); +} + +void ViewManager::requestRSInfo(db_id rsId) +{ + DEBUG_ENTRY; + RSJobViewer *viewer = nullptr; + + auto it = rsHash.constFind(rsId); + if(it != rsHash.cend()) + { + viewer = it.value(); + } + else + { + //Create a new viewer + viewer = createRSViewer(rsId); + } + + viewer->updateInfo(); + viewer->updatePlan(); + viewer->showNormal(); + viewer->update(); + viewer->raise(); +} + +RSJobViewer *ViewManager::createRSViewer(db_id rsId) +{ + DEBUG_ENTRY; + RSJobViewer *viewer = new RSJobViewer(m_mainWidget); + viewer->setAttribute(Qt::WA_DeleteOnClose); + viewer->setWindowFlag(Qt::Window); + viewer->setRS(rsId); + + viewer->setObjectName(QString("RSJobViewer_%1").arg(rsId)); + + rsHash.insert(rsId, viewer); + connect(viewer, &RSJobViewer::destroyed, this, [this, rsId]() + { + rsHash.remove(rsId); + }); + + return viewer; +} + +void ViewManager::showRSManager() +{ + DEBUG_ENTRY; + + if(rsManager == nullptr) + { + rsManager = new RollingStockManager(m_mainWidget); + connect(rsManager, &RollingStockManager::destroyed, this, [this]() + { + rsManager = nullptr; + }); + + rsManager->setAttribute(Qt::WA_DeleteOnClose); + rsManager->setWindowFlag(Qt::Window); + } + + rsManager->showNormal(); + rsManager->update(); + rsManager->raise(); +} + +void ViewManager::onRSRemoved(db_id rsId) +{ + auto it = rsHash.constFind(rsId); + if(it != rsHash.cend()) + { + it.value()->close(); + rsHash.erase(it); + } +} + +void ViewManager::onRSPlanChanged(db_id rsId) +{ + auto it = rsHash.constFind(rsId); + if(it != rsHash.cend()) + { + RSJobViewer *viewer = it.value(); + viewer->updatePlan(); + viewer->update(); + } +} + +void ViewManager::updateRSPlans(QSet set) +{ + for(auto rsId : set) + { + auto it = rsHash.constFind(rsId); + if(it != rsHash.cend()) + { + RSJobViewer *viewer = it.value(); + viewer->updatePlan(); + viewer->update(); + } + } +} + +void ViewManager::onRSInfoChanged(db_id rsId) +{ + auto it = rsHash.constFind(rsId); + if(it != rsHash.cend()) + { + RSJobViewer *viewer = it.value(); + viewer->updateInfo(); + viewer->update(); + } +} + +void ViewManager::showStationsManager() +{ + DEBUG_ENTRY; + if(stManager == nullptr) + { + stManager = new StationsManager(m_mainWidget); + connect(stManager, &StationsManager::destroyed, this, [this]() + { + stManager = nullptr; + }); + + stManager->setAttribute(Qt::WA_DeleteOnClose); + stManager->setWindowFlag(Qt::Window); + } + + stManager->showNormal(); + stManager->update(); + stManager->raise(); +} + +void ViewManager::onStRemoved(db_id stId) +{ + auto it = stHash.constFind(stId); + if(it != stHash.cend()) + { + it.value()->close(); + stHash.erase(it); + } + + auto it2 = stRSHash.constFind(stId); + if(it2 != stRSHash.cend()) + { + it2.value()->close(); + stRSHash.erase(it2); + } +} + +void ViewManager::onStNameChanged(db_id stId) +{ + //If there is a StationJobViewer window open for this station, update it's title (= new station name) + auto it = stHash.constFind(stId); + if(it != stHash.cend()) + { + it.value()->updateName(); + } + + //Same for StationFreeRSViewer + auto it2 = stRSHash.constFind(stId); + if(it2 != stRSHash.cend()) + { + it2.value()->updateTitle(); + } +} + +void ViewManager::onStPlanChanged(db_id stId) +{ + //If there is a StationJobViewer window open for this station, update it's contents + auto it = stHash.constFind(stId); + if(it != stHash.cend()) + { + it.value()->updateJobsList(); + } + + auto it2 = stRSHash.constFind(stId); + if(it2 != stRSHash.cend()) + { + it2.value()->updateData(); + } +} + +StationJobView* ViewManager::createStJobViewer(db_id stId) +{ + DEBUG_ENTRY; + StationJobView *viewer = new StationJobView(m_mainWidget); + viewer->setAttribute(Qt::WA_DeleteOnClose); + viewer->setWindowFlag(Qt::Window); + viewer->setStation(stId); + + viewer->setObjectName(QString("StationJobView_%1").arg(stId)); + + stHash.insert(stId, viewer); + connect(viewer, &StationJobView::destroyed, this, [this, stId]() + { + stHash.remove(stId); + }); + + return viewer; +} + +void ViewManager::requestStPlan(db_id stId) +{ + DEBUG_ENTRY; + StationJobView *viewer = nullptr; + + auto it = stHash.constFind(stId); + if(it != stHash.constEnd()) + { + viewer = it.value(); + } + else + { + viewer = createStJobViewer(stId); + } + + viewer->updateName(); + viewer->updateJobsList(); + + viewer->showNormal(); + viewer->update(); + viewer->raise(); +} + +StationFreeRSViewer* ViewManager::createStFreeRSViewer(db_id stId) +{ + DEBUG_ENTRY; + StationFreeRSViewer *viewer = new StationFreeRSViewer(m_mainWidget); + viewer->setAttribute(Qt::WA_DeleteOnClose); + viewer->setWindowFlag(Qt::Window); + viewer->setStation(stId); + + viewer->setObjectName(QString("StationFreeRSViewer_%1").arg(stId)); + + stRSHash.insert(stId, viewer); + connect(viewer, &StationFreeRSViewer::destroyed, this, [this, stId]() + { + stRSHash.remove(stId); + }); + + return viewer; +} + +void ViewManager::requestStFreeRSViewer(db_id stId) +{ + DEBUG_ENTRY; + StationFreeRSViewer *viewer = nullptr; + + auto it = stRSHash.constFind(stId); + if(it != stRSHash.constEnd()) + { + viewer = it.value(); + } + else + { + viewer = createStFreeRSViewer(stId); + } + + viewer->updateTitle(); + viewer->updateData(); + + viewer->showNormal(); + viewer->update(); + viewer->raise(); +} + +void ViewManager::showShiftManager() +{ + DEBUG_ENTRY; + if(shiftManager == nullptr) + { + shiftManager = new ShiftManager(m_mainWidget); + connect(shiftManager, &ShiftManager::destroyed, this, [this]() + { + shiftManager = nullptr; + }); + + shiftManager->setAttribute(Qt::WA_DeleteOnClose); + shiftManager->setWindowFlag(Qt::Window); + } + + //shiftManager->updateModel(); + + shiftManager->showNormal(); + shiftManager->update(); + shiftManager->raise(); +} + +void ViewManager::showJobsManager() +{ + if(jobsManager == nullptr) + { + jobsManager = new JobsManager(m_mainWidget); + + connect(jobsManager, &JobsManager::destroyed, this, [this]() + { + jobsManager = nullptr; + }); + + jobsManager->setAttribute(Qt::WA_DeleteOnClose); + jobsManager->setWindowFlag(Qt::Window); + } + + jobsManager->showNormal(); + jobsManager->raise(); +} + +void ViewManager::showSessionStartEndRSViewer() +{ + if(!sessionRSViewer) + { + sessionRSViewer = new SessionStartEndRSViewer(m_mainWidget); + connect(sessionRSViewer, &QObject::destroyed, this, [this](){ + sessionRSViewer = nullptr; + }); + + sessionRSViewer->setAttribute(Qt::WA_DeleteOnClose); + sessionRSViewer->setWindowFlag(Qt::Window); + } + + sessionRSViewer->showNormal(); + sessionRSViewer->raise(); +} + +void ViewManager::resumeRSImportation() +{ + RollingStockManager::importRS(true, m_mainWidget); +} + +ShiftViewer* ViewManager::createShiftViewer(db_id id) +{ + ShiftViewer *viewer = new ShiftViewer(m_mainWidget); + viewer->setAttribute(Qt::WA_DeleteOnClose); + viewer->setWindowFlag(Qt::Window); + + viewer->setShift(id); + + shiftHash.insert(id, viewer); + connect(viewer, &ShiftViewer::destroyed, this, [this, id]() + { + shiftHash.remove(id); + }); + + return viewer; +} + +GraphManager *ViewManager::getGraphMgr() const +{ + return mGraphMgr; +} + +void ViewManager::requestShiftViewer(db_id id) +{ + ShiftViewer *viewer = nullptr; + auto it = shiftHash.constFind(id); + if(it != shiftHash.constEnd()) + { + viewer = it.value(); + } + else + { + viewer = createShiftViewer(id); + } + + viewer->updateJobsModel(); + + viewer->showNormal(); + viewer->update(); + viewer->raise(); +} + +void ViewManager::onShiftRemoved(db_id shiftId) +{ + auto it = shiftHash.constFind(shiftId); + if(it != shiftHash.constEnd()) + { + it.value()->close(); + shiftHash.erase(it); + } +} + +void ViewManager::onShiftEdited(db_id shiftId) +{ + auto it = shiftHash.constFind(shiftId); + if(it != shiftHash.constEnd()) + { + it.value()->updateName(); + } +} + +void ViewManager::onShiftJobsChanged(db_id shiftId) +{ + auto it = shiftHash.constFind(shiftId); + if(it != shiftHash.constEnd()) + { + it.value()->updateJobsModel(); + } +} + +bool ViewManager::closeEditors() +{ + if(jobEditor && !jobEditor->clearJob()) + { + return false; + } + + if(rsManager && !rsManager->close()) + { + return false; + } + + qDeleteAll(rsHash); + rsHash.clear(); + + if(stManager && !stManager->close()) + { + return false; + } + + qDeleteAll(stHash); + stHash.clear(); + + qDeleteAll(stRSHash); + stRSHash.clear(); + + + if(shiftManager && !shiftManager->close()) + { + return false; + } + + if(shiftGraphEditor) + { + //Delete immidiately so ShiftGraphHolder gets deleted and releases queries + //Calling 'close()' with WA_DeleteOnClose calls 'deleteLater()' so it gets deleted after MeetingSession tries to close database + delete shiftGraphEditor; + shiftGraphEditor = nullptr; + } + + qDeleteAll(shiftHash); + shiftHash.clear(); + + return true; +} + +bool ViewManager::requestJobSelection(db_id jobId, bool select, bool ensureVisible) const +{ + db_id curLineId = mGraphMgr->getCurLineId(); + + //Try to select first available segment in current line + //If the job has no segment in this line, switch current line to first job segment + + db_id segmentId = 0; + db_id lineId = 0; + query q(Session->m_Db, "SELECT id,lineId FROM jobsegments WHERE jobId=? ORDER BY num"); + q.bind(1, jobId); + for(auto r : q) + { + db_id segId = r.get(0); + db_id segLineId = r.get(1); + if(!lineId) + { + //Save first segment in case we cannot find one in current line + segmentId = segId; + lineId = segLineId; + } + if(segLineId == curLineId) + { + //Fount one in current line, stop search + segmentId = segId; + lineId = segLineId; + break; + } + } + + if(!segmentId) + return false; //Job doesn't seem to have a path, or it doesn't exist! + + //Clear previous selection to avoid multiple selection + mGraphMgr->clearSelection(); + + if(curLineId != lineId) + { + //Change current line + if(!mGraphMgr->setCurrentLine(lineId)) + return false; + } + + return Session->mJobStorage->selectSegment(jobId, segmentId, select, ensureVisible); +} + +//Move to prev/next segment of selected job: changes current line +bool ViewManager::requestJobShowPrevNextSegment(bool prev, bool select, bool ensureVisible) +{ + JobSelection sel = mGraphMgr->getSelectedJob(); + if(sel.jobId == 0 || sel.segmentId == 0) + return false; //No selected Job + + query q(Session->m_Db); + if(prev) + { + q.prepare("SELECT s.id,s.lineId,MAX(s.num) FROM jobsegments s JOIN jobsegments s1 ON s1.id=?" + " WHERE s.jobId=? AND s.nums1.num"); + } + q.bind(1, sel.segmentId); + q.bind(2, sel.jobId); + q.step(); + auto r = q.getRows(); + if(r.column_type(0) == SQLITE_NULL) + return false; //Alredy First segment (or Last if going forward) + + db_id segmentId = r.get(0); + db_id lineId = r.get(1); + + //Change current line + if(!mGraphMgr->setCurrentLine(lineId)) + return false; + + return Session->mJobStorage->selectSegment(sel.jobId, segmentId, select, ensureVisible); +} + +bool ViewManager::requestJobEditor(db_id jobId, db_id stopId) +{ + if(!jobEditor || jobId == 0) + return false; + + if(!jobEditor->setJob(jobId)) + return false; + + jobEditor->parentWidget()->show(); //DockWidget must be visible + jobEditor->setEnabled(true); + jobEditor->show(); + + if(stopId) + { + jobEditor->selectStop(stopId); + } + + return true; +} + +bool ViewManager::requestJobCreation() +{ + /* Creates a new job and opens JobPathEditor */ + + if(!jobEditor || !jobEditor->createNewJob()) + return false; //JobPathEditor is busy, abort + + jobEditor->parentWidget()->show(); //DockWidget must be visible + jobEditor->setEnabled(true); + jobEditor->show(); + + return true; +} + +bool ViewManager::requestClearJob(bool evenIfEditing) +{ + if(!jobEditor) + return false; + + if(jobEditor->isEdited() && !evenIfEditing) + return false; + + if(!jobEditor->clearJob()) + return false; + + jobEditor->setEnabled(false); + return true; +} + +bool ViewManager::removeSelectedJob() +{ + db_id jobId = mGraphMgr->getSelectedJob().jobId; + if(jobId == 0) + return false; + + if(jobEditor) + { + if(jobEditor->currentJobId() == jobId) + { + if(!jobEditor->clearJob()) + { + requestJobSelection(jobId, true, false); + return false; + } + jobEditor->setEnabled(false); + } + } + + Session->mJobStorage->removeJob(jobId); + + return true; +} + +void ViewManager::requestShiftGraphEditor() +{ + if(shiftGraphEditor == nullptr) + { + shiftGraphEditor = new ShiftGraphEditor(m_mainWidget); + + connect(shiftGraphEditor, &ShiftGraphEditor::destroyed, this, [this]() + { + shiftGraphEditor = nullptr; + }); + + shiftGraphEditor->setAttribute(Qt::WA_DeleteOnClose); + shiftGraphEditor->setWindowFlag(Qt::Window); + } + + shiftGraphEditor->showNormal(); + shiftGraphEditor->update(); + shiftGraphEditor->raise(); +} + +void ViewManager::prepareQueries() +{ + if(jobEditor) + jobEditor->prepareQueries(); +} + +void ViewManager::finalizeQueries() +{ + if(jobEditor) + jobEditor->finalizeQueries(); +} + +void ViewManager::onGraphOptionsChanged() +{ + //TODO: remove this function, handle directly inside the models, draw line before jobs + Session->mLineStorage->redrawAllLines(); + Session->mJobStorage->drawJobs(); +} diff --git a/src/viewmanager/viewmanager.h b/src/viewmanager/viewmanager.h new file mode 100644 index 0000000..3320971 --- /dev/null +++ b/src/viewmanager/viewmanager.h @@ -0,0 +1,106 @@ +#ifndef VIEWMANAGER_H +#define VIEWMANAGER_H + +#include + +#include + +#include "utils/types.h" + +class RSJobViewer; +class RollingStockManager; +class StationsManager; +class StationJobView; +class StationFreeRSViewer; +class ShiftManager; +class ShiftViewer; +class JobPathEditor; +class ShiftGraphEditor; +class GraphManager; +class JobsManager; +class SessionStartEndRSViewer; + +class ViewManager : public QObject +{ + Q_OBJECT + + friend class MainWindow; +public: + explicit ViewManager(QObject *parent = nullptr); + + void prepareQueries(); + void finalizeQueries(); + + bool closeEditors(); + + GraphManager *getGraphMgr() const; + + bool requestJobSelection(db_id jobId, bool select, bool ensureVisible) const; + bool requestJobEditor(db_id jobId, db_id stopId = 0); + bool requestJobCreation(); + bool requestClearJob(bool evenIfEditing = false); + bool removeSelectedJob(); + + bool requestJobShowPrevNextSegment(bool prev, bool select = true, bool ensureVisible = true); + + void updateRSPlans(QSet set); + + void requestRSInfo(db_id rsId); + void requestStPlan(db_id stId); + void requestStFreeRSViewer(db_id stId); + void requestShiftViewer(db_id id); + void requestShiftGraphEditor(); + + void showRSManager(); + void showStationsManager(); + void showShiftManager(); + void showJobsManager(); + + void showSessionStartEndRSViewer(); + + void resumeRSImportation(); + +private slots: + void onGraphOptionsChanged(); + + void onRSRemoved(db_id rsId); + void onRSPlanChanged(db_id rsId); + void onRSInfoChanged(db_id rsId); + + void onStRemoved(db_id stId); + void onStNameChanged(db_id stId); + void onStPlanChanged(db_id stId); + + void onShiftRemoved(db_id shiftId); + void onShiftEdited(db_id shiftId); + void onShiftJobsChanged(db_id shiftId); + +private: + RSJobViewer* createRSViewer(db_id rsId); + StationJobView *createStJobViewer(db_id stId); + StationFreeRSViewer *createStFreeRSViewer(db_id stId); + ShiftViewer *createShiftViewer(db_id id); + +private: + GraphManager *mGraphMgr; + + QWidget *m_mainWidget; + + RollingStockManager *rsManager; + QHash rsHash; + + StationsManager *stManager; + QHash stHash; + QHash stRSHash; + + ShiftManager *shiftManager; + QHash shiftHash; + ShiftGraphEditor *shiftGraphEditor; + + JobPathEditor *jobEditor; + JobsManager *jobsManager; + + SessionStartEndRSViewer *sessionRSViewer; +}; + +#endif // VIEWMANAGER_H