Class: MppReader::Reader14
- Inherits:
-
Object
- Object
- MppReader::Reader14
- Defined in:
- lib/mpp_reader/reader14.rb
Overview
Reads tasks and resources from an MPP14 project directory. Ported from MPXJ MPP14Reader (processTaskData/createTaskMap, processResourceData/ createResourceMap).
Constant Summary collapse
- PROJECT_DIR =
" 114"- TASK_FIXED_META_ITEM_SIZE =
47- TASK_FIXED2_META_ITEM_SIZES =
[92, 93, 94, 95, 96].freeze
- RESOURCE_FIXED_META_ITEM_SIZE =
37- NULL_TASK_BLOCK_SIZE =
16- DELETED_FLAG =
0x02- KEEP_EXISTING_FLAG =
0x04- ASSIGNMENT_FIXED_META_ITEM_SIZE =
34- ASSIGNMENT_FIXED_DATA_ITEM_SIZE =
110- ASSIGNMENT_FIXED2_DATA_ITEM_SIZE =
48- NULL_RESOURCE_ID =
-65_535
- MILESTONE_FLAG =
Project 2016 task meta-data bit flags (MPXJ PROJECT2016_TASK_META_DATA_BIT_FLAGS / *_META_DATA2_BIT_FLAGS, core subset). Files written by older Project versions use slightly different locations; not yet ported (the corpus this gem targets is written by Project 2016+).
[10, 0x02].freeze
- ACTIVE_FLAG =
in Fixed2Meta records
[8, 0x40].freeze
- MANUALLY_SCHEDULED_FLAG =
[8, 0x80].freeze
Instance Method Summary collapse
-
#initialize(cfbf) ⇒ Reader14
constructor
A new instance of Reader14.
-
#read_assignments(task_uids) ⇒ Object
Ported from MPXJ ResourceAssignmentFactory#process.
-
#read_calendars ⇒ Object
Reads calendars from TBkndCal (ported from MPXJ AbstractCalendarFactory and AbstractCalendarAndExceptionFactory; Project 2013+ record layout).
-
#read_relations(tasks_by_uid) ⇒ Object
Reads predecessor links from TBkndCons and wires them into the given tasks (by unique id).
- #read_resources ⇒ Object
- #read_tasks ⇒ Object
Constructor Details
#initialize(cfbf) ⇒ Reader14
Returns a new instance of Reader14.
30 31 32 33 34 35 36 37 |
# File 'lib/mpp_reader/reader14.rb', line 30 def initialize(cfbf) @cfbf = cfbf props_stream = stream("Props") raise InvalidFormatError, "missing project Props stream" if props_stream.nil? @props = Blocks::Props.new(props_stream) @field_reader = FieldReader.new(@props) end |
Instance Method Details
#read_assignments(task_uids) ⇒ Object
Ported from MPXJ ResourceAssignmentFactory#process. task_uids gates assignments to known tasks (MPXJ skips assignments whose task is absent from the file).
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/mpp_reader/reader14.rb', line 93 def read_assignments(task_uids) field_map = FieldMap.for_assignments(@props) = Blocks::VarMeta.new(stream("TBkndAssn/VarMeta")) var_data = Blocks::Var2Data.new(, stream("TBkndAssn/Var2Data")) = Blocks::FixedMeta.new(stream("TBkndAssn/FixedMeta"), ASSIGNMENT_FIXED_META_ITEM_SIZE) fixed_data = Blocks::FixedData.fixed_size(stream("TBkndAssn/FixedData"), ASSIGNMENT_FIXED_DATA_ITEM_SIZE) fixed2_data = Blocks::FixedData.fixed_size(stream("TBkndAssn/Fixed2Data"), ASSIGNMENT_FIXED2_DATA_ITEM_SIZE) uid_offset = field_map[:unique_id]&.fixed_offset raise InvalidFormatError, "assignment field map lacks a unique_id location" if uid_offset.nil? assignments = [] .item_count.times do || = [] next if .nil? || .getbyte(0) != 0 # deleted index = fixed_data.index_from_offset(.byteslice(4, 4).unpack1("V")) next if index.nil? record = fixed_data[index] record = record.ljust(field_map.max_fixed_data_size(0), "\0") if record.bytesize < field_map.max_fixed_data_size(0) unique_id = record.byteslice(uid_offset, 4).unpack1("l<") next unless .entries?(unique_id) assignment = build_assignment(field_map, unique_id, [record, fixed2_data[index]], var_data) next unless task_uids.include?(assignment.task_unique_id) assignments << assignment end assignments end |
#read_calendars ⇒ Object
Reads calendars from TBkndCal (ported from MPXJ AbstractCalendarFactory and AbstractCalendarAndExceptionFactory; Project 2013+ record layout). Calendar var data: seven 60-byte day blocks then exception records.
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/mpp_reader/reader14.rb', line 184 def read_calendars = Blocks::VarMeta.new(stream("TBkndCal/VarMeta")) var_data = Blocks::Var2Data.new(, stream("TBkndCal/Var2Data")) = Blocks::FixedMeta.new(stream("TBkndCal/FixedMeta"), 10) fixed_data = Blocks::FixedData.new(, stream("TBkndCal/FixedData"), max_expected_size: 12) default_data = @props[DEFAULT_CALENDAR_HOURS_KEY] project15 = application_version > 14 id_offset = project15 ? 8 : 0 base_offset = project15 ? 0 : 4 resource_offset = project15 ? 4 : 8 calendars = {} fixed_data.item_count.times do |index| record = fixed_data[index] next if record.nil? || record.bytesize < 8 offset = 0 while offset + 12 <= record.bytesize unique_id = record.byteslice(offset + id_offset, 4).unpack1("V") base_id = record.byteslice(offset + base_offset, 4).unpack1("l<") resource_id = record.byteslice(offset + resource_offset, 4).unpack1("l<") offset += 12 next if unique_id <= 0 || calendars.key?(unique_id) data = var_data.bytes(unique_id, CALENDAR_DATA_KEY) calendar = Calendar.new calendar.unique_id = unique_id is_base = base_id <= 0 || base_id == unique_id # Only base calendars take their name from the file; derived # (resource) calendar name entries are often stale, so MPXJ # ignores them and names the calendar after its resource. calendar.name = var_data.string(unique_id, CALENDAR_NAME_KEY) if is_base calendar.base_calendar_unique_id = base_id unless is_base # Derived (resource) calendars keep the link even for resource id # 0 - the unnamed placeholder resource is a real, linkable entry. calendar.resource_unique_id = resource_id if !is_base || resource_id.positive? data = default_data if data.nil? && is_base read_calendar_days(data, calendar, is_base) read_calendar_exceptions(data, calendar) calendars[unique_id] = calendar end end calendars.values end |
#read_relations(tasks_by_uid) ⇒ Object
Reads predecessor links from TBkndCons and wires them into the given tasks (by unique id). Ported from MPXJ ConstraintFactory. Layout: uid u32@0, predecessor u32@4, successor u32@8, type u16@12; lag location depends on writer version (Project 2010 vs 2013+).
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/mpp_reader/reader14.rb', line 129 def read_relations(tasks_by_uid) = stream("TBkndCons/FixedMeta") return if .nil? = Blocks::FixedMeta.new(, 10) fixed_data = Blocks::FixedData.(, stream("TBkndCons/FixedData"), 20) project15 = application_version > 14 lag_offset = project15 ? 14 : 16 lag_units_offset = project15 ? 18 : 14 seen = {} # NOTE: the header count, not the block-derived count - MS Project # leaves stale link records past the header count (MPXJ iterates # getItemCount() here, unlike for tasks). [.header_item_count, .item_count].min.times do |index| = [index] next if .nil? || .byteslice(0, 2).unpack1("v") != 0 # deleted data_index = fixed_data.index_from_offset(.byteslice(4, 4).unpack1("V")) next if data_index.nil? record = fixed_data[data_index] next if record.nil? || record.bytesize < 14 predecessor_uid = record.byteslice(4, 4).unpack1("V") successor_uid = record.byteslice(8, 4).unpack1("V") # Links to the project summary task and self-links are not valid. next if predecessor_uid.zero? || successor_uid.zero? || predecessor_uid == successor_uid predecessor = tasks_by_uid[predecessor_uid] successor = tasks_by_uid[successor_uid] next if predecessor.nil? || successor.nil? relation = Relation.new relation.unique_id = record.byteslice(0, 4).unpack1("V") relation.predecessor_task_unique_id = predecessor_uid relation.successor_task_unique_id = successor_uid relation.type = Relation::TYPES.fetch(record.byteslice(12, 2).unpack1("v"), :finish_start) units = Decode.duration_units(record.byteslice(lag_units_offset, 2).to_s.unpack1("v").to_i) lag = record.byteslice(lag_offset, 4).to_s.unpack1("l<") relation.lag = lag.nil? ? nil : @field_reader.adjusted_duration(lag, units) # Only one relation per (successor, predecessor, type, lag), as in # MPXJ RelationContainer#addPredecessor. next if seen[[successor_uid, predecessor_uid, relation.type, relation.lag]] seen[[successor_uid, predecessor_uid, relation.type, relation.lag]] = true successor.predecessors << relation predecessor.successors << relation end end |
#read_resources ⇒ Object
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/mpp_reader/reader14.rb', line 70 def read_resources field_map = FieldMap.for_resources(@props) = Blocks::VarMeta.new(stream("TBkndRsc/VarMeta")) var_data = Blocks::Var2Data.new(, stream("TBkndRsc/Var2Data")) = Blocks::FixedMeta.new(stream("TBkndRsc/FixedMeta"), RESOURCE_FIXED_META_ITEM_SIZE) fixed_data = Blocks::FixedData.new(, stream("TBkndRsc/FixedData")) uid_offset = field_map[:unique_id]&.fixed_offset raise InvalidFormatError, "resource field map lacks a unique_id location" if uid_offset.nil? resource_map = build_resource_map(field_map, fixed_data, uid_offset) .unique_ids.filter_map do |unique_id| index = resource_map[unique_id] next if index.nil? build_resource(field_map, unique_id, [fixed_data[index]], var_data) end end |
#read_tasks ⇒ Object
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/mpp_reader/reader14.rb', line 39 def read_tasks field_map = FieldMap.for_tasks(@props) = Blocks::VarMeta.new(stream("TBkndTask/VarMeta")) var_data = Blocks::Var2Data.new(, stream("TBkndTask/Var2Data")) = Blocks::FixedMeta.new(stream("TBkndTask/FixedMeta"), TASK_FIXED_META_ITEM_SIZE) fixed_data = Blocks::FixedData.new(, stream("TBkndTask/FixedData"), max_expected_size: field_map.max_fixed_data_size(0)) = Blocks::FixedMeta.with_derived_item_size( stream("TBkndTask/Fixed2Meta"), TASK_FIXED2_META_ITEM_SIZES, fixed_data.item_count ) fixed2_data = Blocks::FixedData.new(, stream("TBkndTask/Fixed2Data")) uid_offset = field_map[:unique_id]&.fixed_offset raise InvalidFormatError, "task field map lacks a unique_id location" if uid_offset.nil? task_map = build_task_map(field_map, , fixed_data, fixed2_data, , uid_offset) tasks = task_map.sort.filter_map do |unique_id, index| next if index.nil? record = fixed_data[index] next if record.nil? || record.bytesize == NULL_TASK_BLOCK_SIZE build_task(field_map, unique_id, [record, fixed2_data[index]], [index], [index], var_data) end link_hierarchy(tasks.sort_by! { |t| t.id || 0 }) tasks end |