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.telephony.sats2range.read.SatS2RangeFileFormat; 20 import com.android.telephony.sats2range.read.SatS2RangeFileReader; 21 import com.android.telephony.sats2range.read.SuffixTableRange; 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.Comparator; 33 import java.util.HashMap; 34 import java.util.Iterator; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.Objects; 38 import java.util.Scanner; 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 two numbers separated by a comma. The first number is in the 46 * range of a 64bit number which represents the ID of a S2 cell. The second 47 * number is satellite access config ID for the S2 cell. 48 * @param s2Level The S2 level of all S2 cells in the input file. 49 * @param isAllowedList {@code true} means the input file contains an allowed list of S2 cells. 50 * {@code false} means the input file contains a disallowed list of S2 51 * cells. 52 * @param outputFile The output file to which the satellite S2 data in block format will be 53 * written. 54 */ create(String inputFile, int s2Level, boolean isAllowedList, int entryValueSizeInBytes, int versionNumber, String outputFile)55 public static void create(String inputFile, int s2Level, boolean isAllowedList, 56 int entryValueSizeInBytes, int versionNumber, String outputFile) throws Exception { 57 // Read a list of S2 cells from input file 58 List<Pair<Long, Integer>> s2Cells = readS2CellsFromFile(inputFile); 59 System.out.println("Number of S2 cells read from file:" + s2Cells.size()); 60 61 // Convert the input list of S2 Cells into the list of sorted S2CellId 62 System.out.println("Denormalizing S2 Cell IDs to the expected s2 level=" + s2Level); 63 List<Pair<S2CellId, Integer>> sortedS2CellIds = normalize(s2Cells, s2Level); 64 65 // IDs of S2CellId are converted to unsigned long numbers, which will be then used to 66 // compare S2CellId. 67 sortedS2CellIds.sort(Comparator.comparing(o -> o.first)); 68 System.out.println("Number of S2 cell IDs:" + sortedS2CellIds.size()); 69 70 // Compress the list of S2CellId into S2 ranges 71 List<SatS2Range> satS2Ranges = createSatS2Ranges(sortedS2CellIds, s2Level); 72 73 // Write the S2 ranges into a block file 74 SatS2RangeFileFormat fileFormat = FileFormats.getFileFormatForLevel(s2Level, isAllowedList, 75 entryValueSizeInBytes, versionNumber); 76 try (SatS2RangeFileWriter satS2RangeFileWriter = 77 SatS2RangeFileWriter.open(new File(outputFile), fileFormat)) { 78 Iterator<SuffixTableRange> suffixTableRangeIterator = satS2Ranges 79 .stream() 80 .map(x -> new SuffixTableRange(x.rangeStart.id(), x.rangeEnd.id(), 81 x.entryValue)) 82 .iterator(); 83 /* 84 * Group the sorted ranges into contiguous suffix blocks. Big ranges might get split as 85 * needed to fit them into suffix blocks. 86 */ 87 satS2RangeFileWriter.createSortedSuffixBlocks(suffixTableRangeIterator); 88 } 89 90 // Validate the output block file 91 System.out.println("Validating the output block file..."); 92 try (SatS2RangeFileReader satS2RangeFileReader = 93 SatS2RangeFileReader.open(new File(outputFile))) { 94 if (isAllowedList != satS2RangeFileReader.isAllowedList()) { 95 throw new IllegalStateException("isAllowedList=" 96 + satS2RangeFileReader.isAllowedList() + " does not match the input " 97 + "argument=" + isAllowedList); 98 } 99 100 // Verify that all input S2 cells are present in the output block file and the their 101 // entry value matches the provided entry value 102 for (Pair<S2CellId, Integer> s2CellInfo : sortedS2CellIds) { 103 SuffixTableRange entry = 104 satS2RangeFileReader.findEntryByCellId(s2CellInfo.first.id()); 105 if (entry == null) { 106 throw new IllegalStateException("s2CellInfo=" + s2CellInfo 107 + " is not present in the output sat s2 file"); 108 } else if (entry.getEntryValue() != s2CellInfo.second) { 109 throw new IllegalStateException("entry.getEntryValue=" + entry.getEntryValue() 110 + " does not match the provided entry value=" + s2CellInfo.second); 111 } 112 } 113 114 // Verify the cell right before the first cell in the sortedS2CellIds is not present in 115 // the output block file 116 S2CellId prevCell = sortedS2CellIds.getFirst().first.prev(); 117 boolean containsPrevCell = sortedS2CellIds.stream() 118 .anyMatch(pair -> pair.first.equals(prevCell)); 119 if (!containsPrevCell 120 && satS2RangeFileReader.findEntryByCellId(prevCell.id()) != null) { 121 throw new IllegalStateException("The cell " + prevCell + ", which is right " 122 + "before the first cell is unexpectedly present in the output sat s2" 123 + " file"); 124 } else { 125 System.out.println("prevCell=" + prevCell + " is in the sortedS2CellIds"); 126 } 127 128 // Verify the cell right after the last cell in the sortedS2CellIds is not present in 129 // the output block file 130 S2CellId nextCell = sortedS2CellIds.getLast().first.next(); 131 boolean containsNextCell = sortedS2CellIds.stream() 132 .anyMatch(pair -> pair.first.equals(nextCell)); 133 if (!containsNextCell 134 && satS2RangeFileReader.findEntryByCellId(nextCell.id()) != null) { 135 throw new IllegalStateException("The cell " + nextCell + ", which is right " 136 + "after the last cell is unexpectedly present in the output sat s2" 137 + " file"); 138 } else { 139 System.out.println("nextCell=" + nextCell + " is in the sortedS2CellIds"); 140 } 141 } 142 System.out.println("Successfully validated the output block file"); 143 } 144 145 /** 146 * Read a list of S2 cells from the inputFile. 147 * 148 * @param inputFile A file containing the list of S2 cells. Each line in the inputFile contains 149 * a 64-bit number - the ID of a S2 cell. 150 * @return A list of S2 cells. 151 */ readS2CellsFromFile(String inputFile)152 private static List<Pair<Long, Integer>> readS2CellsFromFile(String inputFile) 153 throws Exception { 154 List<Pair<Long, Integer>> s2Cells = new ArrayList<>(); 155 InputStream inputStream = new FileInputStream(inputFile); 156 try (Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8)) { 157 while (scanner.hasNextLine()) { 158 String line = scanner.nextLine(); 159 String[] s2CellInfoStrs = line.split(","); 160 if (s2CellInfoStrs == null || s2CellInfoStrs.length != 2) { 161 throw new IllegalStateException("The Input s2 cell file has invalid format, " 162 + "current line=" + line); 163 } 164 try { 165 s2Cells.add(new Pair<>(Long.parseLong(s2CellInfoStrs[0]), 166 Integer.parseUnsignedInt(s2CellInfoStrs[1]))); 167 } catch (Exception ex) { 168 throw new IllegalStateException("Input s2 cell file has invalid format, " 169 + "current line=" + line + ", ex=" + ex); 170 } 171 } 172 } 173 return s2Cells; 174 } 175 176 /** 177 * Convert the list of S2 Cell numbers into the list of S2 Cell IDs at the expected level. 178 */ normalize( List<Pair<Long, Integer>> s2CellNumbers, int s2Level)179 private static List<Pair<S2CellId, Integer>> normalize( 180 List<Pair<Long, Integer>> s2CellNumbers, int s2Level) { 181 Map<S2CellId, Integer> s2CellIdMap = new HashMap<>(); 182 for (Pair<Long, Integer> s2CellInfo : s2CellNumbers) { 183 S2CellId s2CellId = new S2CellId(s2CellInfo.first); 184 if (s2CellId.level() == s2Level) { 185 if (!s2CellIdMap.containsKey(s2CellId)) { 186 s2CellIdMap.put(s2CellId, s2CellInfo.second); 187 } 188 } else if (s2CellId.level() < s2Level) { 189 S2CellId childEnd = s2CellId.childEnd(s2Level); 190 for (s2CellId = s2CellId.childBegin(s2Level); !s2CellId.equals(childEnd); 191 s2CellId = s2CellId.next()) { 192 if (!s2CellIdMap.containsKey(s2CellId)) { 193 s2CellIdMap.put(s2CellId, s2CellInfo.second); 194 } 195 } 196 } else { 197 S2CellId parent = s2CellId.parent(s2Level); 198 if (!s2CellIdMap.containsKey(parent)) { 199 s2CellIdMap.put(parent, s2CellInfo.second); 200 } 201 } 202 } 203 204 List<Pair<S2CellId, Integer>> result = new ArrayList(); 205 for (Map.Entry<S2CellId, Integer> entry : s2CellIdMap.entrySet()) { 206 result.add(new Pair<>(entry.getKey(), entry.getValue())); 207 } 208 return result; 209 } 210 211 /** 212 * Compress the list of sorted S2CellId into S2 ranges. 213 * 214 * @param sortedS2CellIds List of S2CellId sorted in ascending order. 215 * @param s2Level The level of all S2CellId. 216 * @return List of S2 ranges. 217 */ createSatS2Ranges(List<Pair<S2CellId, Integer>> sortedS2CellIds, int s2Level)218 private static List<SatS2Range> createSatS2Ranges(List<Pair<S2CellId, Integer>> sortedS2CellIds, 219 int s2Level) { 220 Stopwatch stopwatch = Stopwatch.createStarted(); 221 List<SatS2Range> ranges = new ArrayList<>(); 222 if (sortedS2CellIds != null && !sortedS2CellIds.isEmpty()) { 223 S2CellId rangeStart = null; 224 S2CellId rangeEnd = null; 225 int rangeEntryValue = 0; 226 for (int i = 0; i < sortedS2CellIds.size(); i++) { 227 S2CellId currentS2CellId = sortedS2CellIds.get(i).first; 228 checkCellIdIsAtLevel(currentS2CellId, s2Level); 229 230 SatS2Range currentRange = createS2Range(currentS2CellId, s2Level, 231 sortedS2CellIds.get(i).second); 232 S2CellId currentS2CellRangeStart = currentRange.rangeStart; 233 S2CellId currentS2CellRangeEnd = currentRange.rangeEnd; 234 235 if (rangeStart == null) { 236 // First time round the loop initialize rangeStart / rangeEnd only. 237 rangeStart = currentS2CellRangeStart; 238 rangeEntryValue = currentRange.entryValue; 239 } else if (rangeEnd.id() != currentS2CellRangeStart.id() 240 || currentRange.entryValue != rangeEntryValue) { 241 // If there's a gap between cellIds or entry values are different, store the 242 // range we have so far and start a new range. 243 ranges.add(new SatS2Range(rangeStart, rangeEnd, rangeEntryValue)); 244 rangeStart = currentS2CellRangeStart; 245 rangeEntryValue = currentRange.entryValue; 246 } 247 rangeEnd = currentS2CellRangeEnd; 248 } 249 ranges.add(new SatS2Range(rangeStart, rangeEnd, rangeEntryValue)); 250 } 251 252 // Sorting the ranges is not necessary. As the input is sorted , it will already be sorted. 253 System.out.printf("Created %s SatS2Ranges in %s milliseconds\n", 254 ranges.size(), stopwatch.elapsed(TimeUnit.MILLISECONDS)); 255 return ranges; 256 } 257 258 /** 259 * @return A pair of S2CellId for the range [s2CellId, s2CellId's next sibling) 260 */ createS2Range(S2CellId s2CellId, int s2Level, int entryValue)261 private static SatS2Range createS2Range(S2CellId s2CellId, int s2Level, int entryValue) { 262 // Since s2CellId is at s2Level, s2CellId.childBegin(s2Level) returns itself. 263 S2CellId firstS2CellRangeStart = s2CellId.childBegin(s2Level); 264 // Get the immediate next sibling of s2CellId 265 S2CellId firstS2CellRangeEnd = s2CellId.childEnd(s2Level); 266 267 if (firstS2CellRangeEnd.face() < firstS2CellRangeStart.face() 268 || !firstS2CellRangeEnd.isValid()) { 269 // Fix this if it becomes an issue. 270 throw new IllegalStateException("firstS2CellId=" + s2CellId 271 + ", childEnd(" + s2Level + ") produced an unsupported" 272 + " value=" + firstS2CellRangeEnd); 273 } 274 return new SatS2Range(firstS2CellRangeStart, firstS2CellRangeEnd, entryValue); 275 } 276 checkCellIdIsAtLevel(S2CellId cellId, int s2Level)277 private static void checkCellIdIsAtLevel(S2CellId cellId, int s2Level) { 278 if (cellId.level() != s2Level) { 279 throw new IllegalStateException("Bad level for cellId=" + cellId 280 + ". Must be s2Level=" + s2Level); 281 } 282 } 283 284 /** 285 * A range of S2 cell IDs at a fixed S2 level. The range is expressed as a start cell ID 286 * (inclusive) and an end cell ID (exclusive). 287 */ 288 private static class SatS2Range { 289 public final int entryValue; 290 public final S2CellId rangeStart; 291 public final S2CellId rangeEnd; 292 293 /** 294 * Creates an instance. If the range is invalid or the cell IDs are from different levels 295 * this method throws an {@link IllegalArgumentException}. 296 */ SatS2Range(S2CellId rangeStart, S2CellId rangeEnd, int entryValue)297 SatS2Range(S2CellId rangeStart, S2CellId rangeEnd, int entryValue) { 298 this.entryValue = entryValue; 299 this.rangeStart = Objects.requireNonNull(rangeStart); 300 this.rangeEnd = Objects.requireNonNull(rangeEnd); 301 if (rangeStart.level() != rangeEnd.level()) { 302 throw new IllegalArgumentException( 303 "Levels differ: rangeStart=" + rangeStart + ", rangeEnd=" + rangeEnd); 304 } 305 if (rangeStart.greaterOrEquals(rangeEnd)) { 306 throw new IllegalArgumentException( 307 "Range start (" + rangeStart + " >= range end (" + rangeEnd + ")"); 308 } 309 } 310 } 311 312 /** A basic pair class. */ 313 static class Pair<A, B> { 314 315 public final A first; 316 317 public final B second; 318 Pair(A first, B second)319 Pair(A first, B second) { 320 this.first = first; 321 this.second = second; 322 } 323 } 324 } 325