aboutsummaryrefslogtreecommitdiff
path: root/PagerMessage.cs
blob: 765636c57309e89f49addc51d58086af51fb3e83 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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
124
125
126
127
128
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
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

namespace PagerParser;

public enum AlertLevel {
    Code1 = 1,
    Code3 = 3
}

[Flags]
public enum EmergencyService {
    Ambulance = 0x01,
    Fire      = 0x02,
    Police    = 0x04,
    Rescue    = 0x08,
    SES       = 0x10
}

[Index(nameof(Message))]
public class PagerMessage {
    [JsonIgnore]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int      PagerMessageId { get; set; }
    public DateTime Timestamp      { get; set; }
    public string   Message        { get; set; }

    public virtual ParsedPagerMessage? ParsedMessage { get; set; }

    public override int GetHashCode() =>
        (Timestamp, Message).GetHashCode();
}

public enum MapType {
    Melways                = 1,
    SpacialVisionCentral   = 2,
    SpacialVisionNorthEast = 3,
    SpacialVisionNorthWest = 4,
    SpacialVisionSouthEast = 5,
    SpacialVisionSouthWest = 6
}

[Index(nameof(FirecomJobNo))]
[Index(nameof(PageDestination))]
public class ParsedPagerMessage {
    [JsonIgnore]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int              ParsedPagerMessageId { get; set; }
    public int              FirecomJobNo         { get; set; }
    public string           AssignmentArea       { get; set; }
    public string           JobType              { get; set; }
    public AlertLevel       AlertLevel           { get; set; }
    public string           Description          { get; set; }
    public MapType?         MapType              { get; set; }
    public int?             MapNo                { get; set; }
    public string?          MapGrid              { get; set; }
    public int?             GridReference        { get; set; }
    public EmergencyService AttendingServices    { get; set; }
    public string?          Note                 { get; set; }
    public int?             FireGroundChannel    { get; set; }
    public List<string>     PagedServices        { get; set; } = new();
    public string           PageDestination      { get; set; }

    public virtual GpsPosition? GpsPosition { get; set; }

    [JsonIgnore]
    [ForeignKey(nameof(PagerMessage))]
    public virtual PagerMessage OriginalMessage { get; set; }
}

public interface IPagerMessageParserService {
    public ParsedPagerMessage  Parse(string message);
    public ParsedPagerMessage? TryParse(string message);
}

// We run the pager message parser as a service with a single instance
// as this allows us to pre-compile the parsing regular expression
// which will help when potentially processing 1000's of pager messages.
//
// Running a single instance of the service isn't an issue, as this
// service is only used by the fetch service for parsing new messages.
public class PagerMessageParserService : IPagerMessageParserService {
    public static readonly Dictionary<string, MapType> MapTypeMap = new() {
        { "M",    MapType.Melways                },
        { "SVC",  MapType.SpacialVisionCentral   },
        { "SVNE", MapType.SpacialVisionNorthEast },
        { "SVNW", MapType.SpacialVisionNorthWest },
        { "SVSE", MapType.SpacialVisionSouthEast },
        { "SVSW", MapType.SpacialVisionSouthWest },
    };

    private const string Pattern =
        @"^@@ALERT\s+([A-Z]*[0-9]+)\s+([A-Z&]+)C([13])\s+(\*\s+)?(.*)\s+(M|SVC|SVNE|SVNW|SVSE|SVSW)\s+(\d+)\s+([A-Z]\d+)\s+\((\d+)\)\s+(\*\s+[^*]+\*\s+)?([AFPRS]+)\s+(([A-Z0-9]+\s)+)F([0-9]+)\s+\[([A-Z0-9]+)[_]?\]$";

    private Regex pageMessageRegex =
        new Regex(Pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);

    private Regex firegroundRegex =
        new Regex("^FGD([0-9]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    public ParsedPagerMessage Parse(string message) {
        ParsedPagerMessage m = new();

        var match = pageMessageRegex.Match(message);

        if(!match.Success)
            throw new ArgumentException();

        // Parse non-optional message components
        m.AssignmentArea  = match.Groups[1].Value;
        m.JobType         = match.Groups[2].Value;
        m.Description     = match.Groups[5].Value;
        m.FirecomJobNo    = Int32.Parse(match.Groups[14].Value);
        m.PageDestination = match.Groups[15].Value;

        // Parse optional message components
        if(match.Groups[7].Success)  m.MapNo         = Int32.Parse(match.Groups[7].Value);
        if(match.Groups[8].Success)  m.MapGrid       = match.Groups[8].Value;
        if(match.Groups[9].Success)  m.GridReference = Int32.Parse(match.Groups[9].Value);
        if(match.Groups[10].Success) m.Note          = match.Groups[10].Value;

        // Parse map type using map type dictionary
        if(match.Groups[6].Success) {
            MapTypeMap.TryGetValue(match.Groups[6].Value, out var mapType);
            m.MapType = mapType;
        }

        var pagedServicesField = match.Groups[12].Value.Split(" ");

        // Attempt to extract the fire-ground channel as it can be mixed
        // into the list of paged services.
        m.FireGroundChannel = pagedServicesField
            .Select(x => firegroundRegex.Match(x))
            .Where(m => m.Success)
            .Select(m => (int?) int.Parse(m.Groups[1].Value))
            .DefaultIfEmpty(null)
            .First();

        // Parse the paged services, ignoring the fireground channel
        m.PagedServices = pagedServicesField
            .Where(x => !firegroundRegex.IsMatch(x))
            .Where(x => !string.IsNullOrEmpty(x))
            .ToList();

        switch(match.Groups[3].Value) {
            case "1": m.AlertLevel = AlertLevel.Code1; break;
            case "3": m.AlertLevel = AlertLevel.Code3; break;
            default: throw new ArgumentException();
        }

        // Handle each character to construct a bitmap of attending services
        foreach(char c in match.Groups[11].Value) {
            switch(c) {
                case 'A': m.AttendingServices |= EmergencyService.Ambulance; break;
                case 'F': m.AttendingServices |= EmergencyService.Fire;      break;
                case 'P': m.AttendingServices |= EmergencyService.Police;    break;
                case 'R': m.AttendingServices |= EmergencyService.Rescue;    break;
                case 'S': m.AttendingServices |= EmergencyService.SES;       break;
            }
        }

        return m;
    }

    public ParsedPagerMessage? TryParse(string message) {
        try {
            return Parse(message);
        } catch {
            return null;
        }
    }
}