Initial commit
This commit is contained in:
commit
4cd8197132
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Jan-Lukas Else
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,76 @@
|
||||||
|
# Cell Location Map using Mozilla Location Service
|
||||||
|
|
||||||
|
This repository provides code to display the cell locations using the Mozilla Location Service. The code extracts cell data from a CSV export file and generates an interactive map with markers representing the cell locations.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Go programming language
|
||||||
|
- Access to a [cell export file from the Mozilla Location Service](https://location.services.mozilla.com/downloads)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://git.jlel.se/jlelse/mls.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Change to the project directory:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd mls
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Step 1: Filter Cell Data
|
||||||
|
|
||||||
|
To filter the cell data and generate an output file, run the following command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go run ./filter/main.go <input CSV file> <output CSV file> <radio> <mcc> <network> <min samples> <output columns>
|
||||||
|
```
|
||||||
|
|
||||||
|
- `<input CSV file>`: Path to the input CSV file containing the cell data.
|
||||||
|
- `<output CSV file>`: Path to the output CSV file where the filtered data will be saved.
|
||||||
|
- `<radio>`: The desired radio type (e.g., LTE).
|
||||||
|
- `<mcc>`: The desired Mobile Country Code (e.g., 262 for Germany).
|
||||||
|
- `<network>`: The desired network code (e.g., 3 for o2-de).
|
||||||
|
- `<min samples>`: The minimum number of samples required for a cell to be included.
|
||||||
|
- `<output columns>`: Space-separated list of columns to include in the output.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go run ./filter/main.go ./MLS-full-cell-export.csv ./output.csv LTE 262 3 100 lon lat created updated cell
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Generate Map
|
||||||
|
|
||||||
|
To generate an HTML map with the filtered cell data, run the following command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go run ./web/main.go <input CSV file> <output HTML file>
|
||||||
|
```
|
||||||
|
|
||||||
|
- `<input CSV file>`: Path to the input CSV file containing the filtered cell data with the columns lon,lat,created,updated,cell.
|
||||||
|
- `<output HTML file>`: Path to the output HTML file where the map will be saved.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go run ./web/main.go ./output.csv ./map.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
The code in this repository is based on the [Mozilla Location Service](https://location.services.mozilla.com/) and leverages the following open-source libraries:
|
||||||
|
|
||||||
|
- [Leaflet](https://leafletjs.com/) (version 1.9.4): An open-source JavaScript library for interactive maps.
|
||||||
|
- [Leaflet.markercluster](https://github.com/Leaflet/Leaflet.markercluster) (version 1.4.1): A plugin for Leaflet that provides clustering functionality for markers.
|
||||||
|
|
||||||
|
The idea for this project was inspired by the [CellInspector Map](https://mls.maxomagier.de/map.html).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the [MIT License](LICENSE).
|
|
@ -0,0 +1,126 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Check if the correct number of command-line arguments is provided
|
||||||
|
if len(os.Args) < 6 {
|
||||||
|
fmt.Println("Usage: go run main.go <input CSV file> <output CSV file> <radio> <mcc> <network> <min samples> <output columns>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the command-line arguments
|
||||||
|
inputFile := os.Args[1]
|
||||||
|
outputFile := os.Args[2]
|
||||||
|
desiredRadio := os.Args[3]
|
||||||
|
desiredMCC := os.Args[4]
|
||||||
|
desiredNetwork := os.Args[5]
|
||||||
|
desiredMinSamples := os.Args[6]
|
||||||
|
desiredColumns := os.Args[7:]
|
||||||
|
|
||||||
|
// Open the input CSV file
|
||||||
|
input, err := os.Open(inputFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error opening the input file:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer input.Close()
|
||||||
|
|
||||||
|
// Create the output CSV file
|
||||||
|
output, err := os.Create(outputFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error creating the output file:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer output.Close()
|
||||||
|
|
||||||
|
// CSV reader and writer initialization
|
||||||
|
reader := csv.NewReader(input)
|
||||||
|
writer := csv.NewWriter(output)
|
||||||
|
|
||||||
|
// Read the header row to get the column indices
|
||||||
|
header, err := reader.Read()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error reading CSV header:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
radioIndex := findColumnIndex(header, "radio")
|
||||||
|
mccIndex := findColumnIndex(header, "mcc")
|
||||||
|
netIndex := findColumnIndex(header, "net")
|
||||||
|
samplesIndex := findColumnIndex(header, "samples")
|
||||||
|
|
||||||
|
// Find the indices of the desired columns
|
||||||
|
desiredIndices := make([]int, len(desiredColumns))
|
||||||
|
for i, column := range desiredColumns {
|
||||||
|
desiredIndices[i] = findColumnIndex(header, column)
|
||||||
|
if desiredIndices[i] == -1 {
|
||||||
|
fmt.Println("Column not found:", column)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the header row to the output file
|
||||||
|
err = writer.Write(desiredColumns)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error writing the header row:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the CSV data line by line
|
||||||
|
for {
|
||||||
|
row, err := reader.Read()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
fmt.Println("Error reading CSV data:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the filter criteria
|
||||||
|
radio := row[radioIndex]
|
||||||
|
mcc := row[mccIndex]
|
||||||
|
net := row[netIndex]
|
||||||
|
samples := row[samplesIndex]
|
||||||
|
|
||||||
|
if radio == desiredRadio && mcc == desiredMCC && net == desiredNetwork && samples >= desiredMinSamples {
|
||||||
|
// Extract the desired columns based on indices
|
||||||
|
filteredRow := make([]string, len(desiredIndices))
|
||||||
|
for i, index := range desiredIndices {
|
||||||
|
filteredRow[i] = row[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the row to the output file
|
||||||
|
err := writer.Write(filteredRow)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error writing the row:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write CSV writer data to the file
|
||||||
|
writer.Flush()
|
||||||
|
|
||||||
|
if err := writer.Error(); err != nil {
|
||||||
|
fmt.Println("Error writing the output file:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Filtering completed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to find the index of a column name in the header row
|
||||||
|
func findColumnIndex(header []string, columnName string) int {
|
||||||
|
for i, name := range header {
|
||||||
|
if name == columnName {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
leafletVersion = "1.9.4"
|
||||||
|
markerClusterVersion = "1.4.1"
|
||||||
|
htmlTemplateStart = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Location Map</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@%s/dist/leaflet.css" />
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@%s/dist/MarkerCluster.css" />
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@%s/dist/MarkerCluster.Default.css" />
|
||||||
|
<style>
|
||||||
|
#map {
|
||||||
|
height: 800px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="map"></div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/leaflet@%s/dist/leaflet.js"></script>
|
||||||
|
<script src="https://unpkg.com/leaflet.markercluster@%s/dist/leaflet.markercluster.js"></script>
|
||||||
|
<script>
|
||||||
|
var map = L.map('map').setView([0, 0], 2);
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors',
|
||||||
|
maxZoom: 18,
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
var markers = L.markerClusterGroup();
|
||||||
|
|
||||||
|
`
|
||||||
|
htmlTemplateEnd = `
|
||||||
|
map.addLayer(markers);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) != 3 {
|
||||||
|
fmt.Println("Usage: go run main.go <input CSV file (needs lon,lat,created,updated,cell)> <output HTML file>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inputFile := os.Args[1]
|
||||||
|
outputFile := os.Args[2]
|
||||||
|
|
||||||
|
// Open the CSV file
|
||||||
|
file, err := os.Open(inputFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Read the CSV records
|
||||||
|
reader := csv.NewReader(file)
|
||||||
|
records, err := readCSVRecords(reader)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the HTML
|
||||||
|
html := generateHTML(records)
|
||||||
|
|
||||||
|
// Save the HTML to a file
|
||||||
|
err = writeHTMLToFile(outputFile, html)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Map generated successfully and saved to %s.\n", outputFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readCSVRecords(reader *csv.Reader) ([][]string, error) {
|
||||||
|
reader.Comma = ',' // Set the delimiter to comma explicitly
|
||||||
|
records, err := reader.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the header row
|
||||||
|
if len(records) > 0 {
|
||||||
|
records = records[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateHTML(records [][]string) string {
|
||||||
|
var htmlBuilder strings.Builder
|
||||||
|
htmlBuilder.WriteString(fmt.Sprintf(htmlTemplateStart, leafletVersion, markerClusterVersion, markerClusterVersion, leafletVersion, markerClusterVersion))
|
||||||
|
|
||||||
|
for _, record := range records {
|
||||||
|
// Parse longitude and latitude
|
||||||
|
lon, err := strconv.ParseFloat(record[0], 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error parsing longitude: %v\nLongitude value: %s\n", err, record[0])
|
||||||
|
}
|
||||||
|
lat, err := strconv.ParseFloat(record[1], 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error parsing latitude: %v\nLatitude value: %s\n", err, record[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get created and updated timestamps
|
||||||
|
created := record[2]
|
||||||
|
updated := record[3]
|
||||||
|
cell := record[4]
|
||||||
|
|
||||||
|
htmlBuilder.WriteString(fmt.Sprintf(`
|
||||||
|
var marker = L.marker([%f, %f]);
|
||||||
|
marker.bindTooltip("Created: %s<br />Updated: %s<br />Cell: %s");
|
||||||
|
markers.addLayer(marker);
|
||||||
|
`, lat, lon, formatTimestamp(created), formatTimestamp(updated), cell))
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBuilder.WriteString(htmlTemplateEnd)
|
||||||
|
return htmlBuilder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatTimestamp(timestamp string) string {
|
||||||
|
// Parse the Unix timestamp string to an integer
|
||||||
|
unixTime, err := strconv.ParseInt(timestamp, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "" // Return an empty string if parsing fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the Unix timestamp to a time.Time value
|
||||||
|
timeValue := time.Unix(unixTime, 0)
|
||||||
|
|
||||||
|
// Format the time
|
||||||
|
isoFormatted := timeValue.Format(time.RFC3339)
|
||||||
|
|
||||||
|
return isoFormatted
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeHTMLToFile(filename string, html string) error {
|
||||||
|
file, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = file.WriteString(html)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue