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