1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.telephony.tools.sats2;
18 
19 import com.android.storage.s2.S2LevelRange;
20 import com.android.telephony.sats2range.read.SatS2RangeFileFormat;
21 import com.android.telephony.sats2range.read.SatS2RangeFileReader;
22 import com.android.telephony.sats2range.write.SatS2RangeFileWriter;
23 
24 import com.google.common.base.Stopwatch;
25 import com.google.common.geometry.S2CellId;
26 
27 import java.io.File;
28 import java.io.FileInputStream;
29 import java.io.InputStream;
30 import java.nio.charset.StandardCharsets;
31 import java.util.ArrayList;
32 import java.util.Collections;
33 import java.util.HashSet;
34 import java.util.Iterator;
35 import java.util.List;
36 import java.util.Objects;
37 import java.util.Scanner;
38 import java.util.Set;
39 import java.util.concurrent.TimeUnit;
40 
41 /** A util class for creating a satellite S2 file from the list of S2 cells. */
42 public final class SatS2FileCreator {
43     /**
44      * @param inputFile The input text file containing the list of S2 Cell IDs. Each line in the
45      *                  file contains a number in the range of a 64-bit number which represents the
46      *                  ID of a S2 cell.
47      * @param s2Level The S2 level of all S2 cells in the input file.
48      * @param isAllowedList {@code true} means the input file contains an allowed list of S2 cells.
49      *                      {@code false} means the input file contains a disallowed list of S2
50      *                      cells.
51      * @param outputFile The output file to which the satellite S2 data in block format will be
52      *                   written.
53      */
create(String inputFile, int s2Level, boolean isAllowedList, String outputFile)54     public static void create(String inputFile, int s2Level, boolean isAllowedList,
55             String outputFile) throws Exception {
56         // Read a list of S2 cells from input file
57         List<Long> s2Cells = readS2CellsFromFile(inputFile);
58         System.out.println("Number of S2 cells read from file:" + s2Cells.size());
59 
60         // Convert the input list of S2 Cells into the list of sorted S2CellId
61         System.out.println("Denormalizing S2 Cell IDs to the expected s2 level=" + s2Level);
62         List<S2CellId> sortedS2CellIds = denormalize(s2Cells, s2Level);
63         // IDs of S2CellId are converted to unsigned long numbers, which will be then used to
64         // compare S2CellId.
65         Collections.sort(sortedS2CellIds);
66         System.out.println("Number of S2 cell IDs:" + sortedS2CellIds.size());
67 
68         // Compress the list of S2CellId into S2 ranges
69         List<SatS2Range> satS2Ranges = createSatS2Ranges(sortedS2CellIds, s2Level);
70 
71         // Write the S2 ranges into a block file
72         SatS2RangeFileFormat fileFormat =
73                 FileFormats.getFileFormatForLevel(s2Level, isAllowedList);
74         try (SatS2RangeFileWriter satS2RangeFileWriter =
75                      SatS2RangeFileWriter.open(new File(outputFile), fileFormat)) {
76             Iterator<S2LevelRange> s2LevelRangeIterator = satS2Ranges
77                     .stream()
78                     .map(x -> new S2LevelRange(x.rangeStart.id(), x.rangeEnd.id()))
79                     .iterator();
80             /*
81              * Group the sorted ranges into contiguous suffix blocks. Big ranges might get split as
82              * needed to fit them into suffix blocks.
83              */
84             satS2RangeFileWriter.createSortedSuffixBlocks(s2LevelRangeIterator);
85         }
86 
87         // Validate the output block file
88         System.out.println("Validating the output block file...");
89         try (SatS2RangeFileReader satS2RangeFileReader =
90                      SatS2RangeFileReader.open(new File(outputFile))) {
91             if (isAllowedList != satS2RangeFileReader.isAllowedList()) {
92                 throw new IllegalStateException("isAllowedList="
93                         + satS2RangeFileReader.isAllowedList() + " does not match the input "
94                         + "argument=" + isAllowedList);
95             }
96 
97             // Verify that all input S2 cells are present in the output block file
98             for (S2CellId s2CellId : sortedS2CellIds) {
99                 if (satS2RangeFileReader.findEntryByCellId(s2CellId.id()) == null) {
100                     throw new IllegalStateException("s2CellId=" + s2CellId
101                             + " is not present in the output sat s2 file");
102                 }
103             }
104 
105             // Verify the cell right before the first cell in the sortedS2CellIds is not present in
106             // the output block file
107             S2CellId prevCell = sortedS2CellIds.get(0).prev();
108             if (!sortedS2CellIds.contains(prevCell)
109                     && satS2RangeFileReader.findEntryByCellId(prevCell.id()) != null) {
110                 throw new IllegalStateException("The cell " + prevCell + ", which is right "
111                         + "before the first cell is unexpectedly present in the output sat s2"
112                         + " file");
113             } else {
114                 System.out.println("prevCell=" + prevCell + " is in the sortedS2CellIds");
115             }
116 
117             // Verify the cell right after the last cell in the sortedS2CellIds is not present in
118             // the output block file
119             S2CellId nextCell = sortedS2CellIds.get(sortedS2CellIds.size() - 1).next();
120             if (!sortedS2CellIds.contains(nextCell)
121                     && satS2RangeFileReader.findEntryByCellId(nextCell.id()) != null) {
122                 throw new IllegalStateException("The cell " + nextCell + ", which is right "
123                         + "after the last cell is unexpectedly present in the output sat s2"
124                         + " file");
125             } else {
126                 System.out.println("nextCell=" + nextCell + " is in the sortedS2CellIds");
127             }
128         }
129         System.out.println("Successfully validated the output block file");
130     }
131 
132     /**
133      * Read a list of S2 cells from the inputFile.
134      *
135      * @param inputFile A file containing the list of S2 cells. Each line in the inputFile contains
136      *                  a 64-bit number - the ID of a S2 cell.
137      * @return A list of S2 cells.
138      */
readS2CellsFromFile(String inputFile)139     private static List<Long> readS2CellsFromFile(String inputFile) throws Exception {
140         List<Long> s2Cells = new ArrayList();
141         InputStream inputStream = new FileInputStream(inputFile);
142         try (Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name())) {
143             while (scanner.hasNextLine()) {
144                 String line = scanner.nextLine();
145                 try {
146                     s2Cells.add(Long.parseLong(line));
147                 } catch (Exception ex) {
148                     throw new IllegalStateException("Input s2 cell file has invalid format, "
149                             + "current line=" + line);
150                 }
151             }
152         }
153         return s2Cells;
154     }
155 
156     /**
157      * Convert the list of S2 Cell numbers into the list of S2 Cell IDs at the expected level.
158      */
denormalize(List<Long> s2CellNumbers, int s2Level)159     private static List<S2CellId> denormalize(List<Long> s2CellNumbers, int s2Level) {
160         Set<S2CellId> result = new HashSet<>();
161         for (long s2CellNumber : s2CellNumbers) {
162             S2CellId s2CellId = new S2CellId(s2CellNumber);
163             if (s2CellId.level() == s2Level) {
164                 if (!result.contains(s2CellId)) {
165                     result.add(s2CellId);
166                 }
167             } else if (s2CellId.level() < s2Level) {
168                 S2CellId childEnd = s2CellId.childEnd(s2Level);
169                 for (s2CellId = s2CellId.childBegin(s2Level); !s2CellId.equals(childEnd);
170                         s2CellId = s2CellId.next()) {
171                     if (!result.contains(s2CellId)) {
172                         result.add(s2CellId);
173                     }
174                 }
175             } else {
176                 S2CellId parent = s2CellId.parent(s2Level);
177                 if (!result.contains(parent)) {
178                     result.add(parent);
179                 }
180             }
181         }
182         return new ArrayList(result);
183     }
184 
185     /**
186      * Compress the list of sorted S2CellId into S2 ranges.
187      *
188      * @param sortedS2CellIds List of S2CellId sorted in ascending order.
189      * @param s2Level The level of all S2CellId.
190      * @return List of S2 ranges.
191      */
createSatS2Ranges(List<S2CellId> sortedS2CellIds, int s2Level)192     private static List<SatS2Range> createSatS2Ranges(List<S2CellId> sortedS2CellIds, int s2Level) {
193         Stopwatch stopwatch = Stopwatch.createStarted();
194         List<SatS2Range> ranges = new ArrayList<>();
195         if (sortedS2CellIds != null && sortedS2CellIds.size() > 0) {
196             S2CellId rangeStart = null;
197             S2CellId rangeEnd = null;
198             for (int i = 0; i < sortedS2CellIds.size(); i++) {
199                 S2CellId currentS2CellId = sortedS2CellIds.get(i);
200                 checkCellIdIsAtLevel(currentS2CellId, s2Level);
201 
202                 SatS2Range currentRange = createS2Range(currentS2CellId, s2Level);
203                 S2CellId currentS2CellRangeStart = currentRange.rangeStart;
204                 S2CellId currentS2CellRangeEnd = currentRange.rangeEnd;
205 
206                 if (rangeStart == null) {
207                     // First time round the loop initialize rangeStart / rangeEnd only.
208                     rangeStart = currentS2CellRangeStart;
209                 } else if (rangeEnd.id() != currentS2CellRangeStart.id()) {
210                     // If there's a gap between cellIds, store the range we have so far and start a
211                     // new range.
212                     ranges.add(new SatS2Range(rangeStart, rangeEnd));
213                     rangeStart = currentS2CellRangeStart;
214                 }
215                 rangeEnd = currentS2CellRangeEnd;
216             }
217             ranges.add(new SatS2Range(rangeStart, rangeEnd));
218         }
219 
220         // Sorting the ranges is not necessary. As the input is sorted , it will already be sorted.
221         System.out.printf("Created %s SatS2Ranges in %s milliseconds\n",
222                 ranges.size(), stopwatch.elapsed(TimeUnit.MILLISECONDS));
223         return ranges;
224     }
225 
226     /**
227      * @return A pair of S2CellId for the range [s2CellId, s2CellId's next sibling)
228      */
createS2Range( S2CellId s2CellId, int s2Level)229     private static SatS2Range createS2Range(
230             S2CellId s2CellId, int s2Level) {
231         // Since s2CellId is at s2Level, s2CellId.childBegin(s2Level) returns itself.
232         S2CellId firstS2CellRangeStart = s2CellId.childBegin(s2Level);
233         // Get the immediate next sibling of s2CellId
234         S2CellId firstS2CellRangeEnd = s2CellId.childEnd(s2Level);
235 
236         if (firstS2CellRangeEnd.face() < firstS2CellRangeStart.face()
237                 || !firstS2CellRangeEnd.isValid()) {
238             // Fix this if it becomes an issue.
239             throw new IllegalStateException("firstS2CellId=" + s2CellId
240                     + ", childEnd(" + s2Level + ") produced an unsupported"
241                     + " value=" + firstS2CellRangeEnd);
242         }
243         return new SatS2Range(firstS2CellRangeStart, firstS2CellRangeEnd);
244     }
245 
checkCellIdIsAtLevel(S2CellId cellId, int s2Level)246     private static void checkCellIdIsAtLevel(S2CellId cellId, int s2Level) {
247         if (cellId.level() != s2Level) {
248             throw new IllegalStateException("Bad level for cellId=" + cellId
249                     + ". Must be s2Level=" + s2Level);
250         }
251     }
252 
253     /**
254      * A range of S2 cell IDs at a fixed S2 level. The range is expressed as a start cell ID
255      * (inclusive) and an end cell ID (exclusive).
256      */
257     private static class SatS2Range {
258         public final S2CellId rangeStart;
259         public final S2CellId rangeEnd;
260 
261         /**
262          * Creates an instance. If the range is invalid or the cell IDs are from different levels
263          * this method throws an {@link IllegalArgumentException}.
264          */
SatS2Range(S2CellId rangeStart, S2CellId rangeEnd)265         SatS2Range(S2CellId rangeStart, S2CellId rangeEnd) {
266             this.rangeStart = Objects.requireNonNull(rangeStart);
267             this.rangeEnd = Objects.requireNonNull(rangeEnd);
268             if (rangeStart.level() != rangeEnd.level()) {
269                 throw new IllegalArgumentException(
270                         "Levels differ: rangeStart=" + rangeStart + ", rangeEnd=" + rangeEnd);
271             }
272             if (rangeStart.greaterOrEquals(rangeEnd)) {
273                 throw new IllegalArgumentException(
274                         "Range start (" + rangeStart + " >= range end (" + rangeEnd + ")");
275             }
276         }
277     }
278 }
279