2024/10/28

Retrofit

因為現今網路服務最常見的就是用 http 協定提供,一般資料面向的服務,也是以 JSON 資料格式作為輸入與輸出的資料格式。Retrofit 定義自己是 type-safe HTTP client for Android and Java。這邊 type-safe 的意思是,透過 Retrofit 的包裝,可以自動將 http request 與 response 資料轉換為 java 類別物件,在自動轉換的過程中,就能以類別的定義,去檢查資料是不是符合類別的定義,而不是未經定義的其他資料。因為是遠端網路服務,Retrofit 提供了同步與非同步兩種使用 http 服務的方式,非同步的呼叫方法可以解決網路延遲,甚至是故障的問題。

測試 http 服務

網路上提供 Fake HTTP API service,有以下網站可以用

GET https://reqres.in/api/users/2

{
    "data": {
        "id": 2,
        "email": "janet.weaver@reqres.in",
        "first_name": "Janet",
        "last_name": "Weaver",
        "avatar": "https://reqres.in/img/faces/2-image.jpg"
    },
    "support": {
        "url": "https://reqres.in/#support-heading",
        "text": "To keep ReqRes free, contributions towards server costs are appreciated!"
    }
}

POST https://reqres.in/api/users

輸入參數

{
    "name": "John",
    "job": "leader"
}

輸出

{
    "name": "morpheus",
    "job": "leader",
    "id": "63",
    "createdAt": "2024-05-22T03:31:37.941Z"
}

Maven pom.xml

在 xml 裡面引用 retrofits 以及 converter-gson 的 library

        <!--retrofit-->
        <dependency>
            <groupId>com.squareup.retrofit2</groupId>
            <artifactId>retrofit</artifactId>
            <version>2.11.0</version>
        </dependency>
        <dependency>
            <groupId>com.squareup.retrofit2</groupId>
            <artifactId>converter-gson</artifactId>
            <version>2.11.0</version>
        </dependency>

同步/非同步

透過 service 產生的 API Call 有 同步/非同步 兩種呼叫方式

  • execute() – Synchronously send the request and return its response.
  • enqueue(retrofit2.Callback) – Asynchronously send the request and notify callback of its response or if an error occurred talking to the server, creating the request, or processing the response.

Converter

retrofit2 的 conveter 是用在 http request 與 response 的資料轉換,目前支援這些格式

  • Gsoncom.squareup.retrofit2:converter-gson
  • Jacksoncom.squareup.retrofit2:converter-jackson
  • Moshicom.squareup.retrofit2:converter-moshi
  • Protobufcom.squareup.retrofit2:converter-protobuf
  • Wirecom.squareup.retrofit2:converter-wire
  • Simple XMLcom.squareup.retrofit2:converter-simplexml
  • JAXBcom.squareup.retrofit2:converter-jaxb
  • Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

比較常見的是 json,可以使用 Gson 或 Jackson 協助轉換。

實作

data class

對應剛剛的兩個 service,分別有不同的 data class

User.java

public class User {
    private long id;
    private String first_name;
    private String last_name;
    private String email;
    // getter, setter, toString
}

UserResponse.java

public class UserResponse {
    private User data;
    // getter, setter, toString
}

Account.java

public class Account {
    private String id;
    private String name;
    private String job;
    private Date createdAt;

    // getter, setter, toString
}

service

UserService.java

import retrofit.data.Account;
import retrofit.data.UserResponse;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;

public interface UserService {
    @GET("/api/users/{id}")
    public Call<UserResponse> getUser(@Path("id") long id);

    @POST("/api/users")
    Call<Account> createUser(@Body Account account);
}

main

RetrofitTest.java

import okhttp3.OkHttpClient;
import retrofit.data.Account;
import retrofit.data.UserResponse;
import retrofit.service.UnsafeOkHttpClient;
import retrofit.service.UserService;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.Callback;
import retrofit2.converter.gson.GsonConverterFactory;

public class RetrofitTest {
    public static void sync() {
        OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://reqres.in/")
                .addConverterFactory(GsonConverterFactory.create())
                .client(httpClient.build())
//                .client(UnsafeOkHttpClient.getUnsafeOkHttpClient())
                .build();

        UserService service = retrofit.create(UserService.class);

        // Calling '/api/users/2'
        Call<UserResponse> callSync = service.getUser(2);
        try {
            Response<UserResponse> response = callSync.execute();
            UserResponse apiResponse = response.body();
            System.out.println("sync: "+apiResponse);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        // Calling 'https://reqres.in/api/users'
        Account account = new Account();
        account.setName("John");
        account.setJob("leader");
        Call<Account> callSync2 = service.createUser(account);
        try {
            Response<Account> response2 = callSync2.execute();
            Account apiResponseAccount = response2.body();
            System.out.println(apiResponseAccount);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public static void async() {
        OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://reqres.in/")
                .addConverterFactory(GsonConverterFactory.create())
                .client(httpClient.build())
//                .client(UnsafeOkHttpClient.getUnsafeOkHttpClient())
                .build();

        UserService service = retrofit.create(UserService.class);

        // Calling '/api/users/2'
        Call<UserResponse> callAsync = service.getUser(2);

        callAsync.enqueue(new Callback<>() {
            @Override
            public void onResponse(Call<UserResponse> call, Response<UserResponse> response) {
                int responseCode = response.code();
                UserResponse user = response.body();
                System.out.println("async responseCode="+responseCode+", result=" + user);
            }

            @Override
            public void onFailure(Call<UserResponse> call, Throwable throwable) {
                System.out.println(throwable);
            }
        });

        // Calling 'https://reqres.in/api/users'
        Account account = new Account();
        account.setName("John");
        account.setJob("leader");
        Call<Account> callAsync2 = service.createUser(account);

        callAsync2.enqueue(new Callback<>() {
            @Override
            public void onResponse(Call<Account> call, Response<Account> response) {
                int responseCode = response.code();
                Account accountResponse = response.body();
                System.out.println("async responseCode="+responseCode+", result=" + accountResponse);
            }

            @Override
            public void onFailure(Call<Account> call, Throwable throwable) {
                System.out.println(throwable);
            }
        });
    }

    public static void main(String[] args) {
        sync();
        System.exit(0);
//        async();
    }
}

注意:這邊特定要呼叫 System.exit(0) 來停掉程式,這是因為 Retrofit 內部使用的 OkHttp 採用了 ThreadPoolExecutor,參考這個網址的 issue 討論:Tomcat is not able to stop because of OkHttp ConnectionPool Issue #5542 · square/okhttp · GitHub ,討論寫說沒有直接停掉的方法。

裡面有說到大約 6mins 後就會 Conenction Pool 停掉。實際上實測,大約等了 5mins。

目前如果要調整這個問題,必須要覆蓋 connectionPool 的原始設定。方法是在產生 OkHttpClient.Builder() 的時候,指定一個新的 ConnectionPool,並將參數 keepAliveDurationMills 改短。

        int maxIdleConnections = 10;
        int keepAliveDurationMills = 1000;
        OkHttpClient.Builder httpClient = new OkHttpClient.Builder()
                .connectionPool(new ConnectionPool(maxIdleConnections, keepAliveDurationMills, TimeUnit.MILLISECONDS));

這樣修改,只有在同步呼叫時有用。如果是非同步呼叫,程式還是會等 3mins 才會停下來。

自訂 Http Client

有時候在開發時,https 網站會採用自己產生的 SSL 憑證,這時候需要調整 http client,不檢查 domain 來源

UnsafeOkHttpClient.java

import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

import javax.net.ssl.*;
import java.security.cert.CertificateException;
import java.util.concurrent.TimeUnit;

public class UnsafeOkHttpClient {

    public static OkHttpClient getUnsafeOkHttpClient() {
        try {
            // Create a trust manager that does not validate certificate chains
            final TrustManager[] trustAllCerts = new TrustManager[]{
                    new X509TrustManager() {
                        @Override
                        public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {}

                        @Override
                        public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {}

                        @Override
                        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                            return new java.security.cert.X509Certificate[]{};
                        }
                    }
            };

            // Install the all-trusting trust manager
            final SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
            // Create an ssl socket factory with our all-trusting manager
            final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

            int maxIdleConnections = 10;
            int keepAliveDurationMills = 1000;
            OkHttpClient.Builder builder = new OkHttpClient.Builder()
                    .connectionPool(new ConnectionPool(maxIdleConnections, keepAliveDurationMills, TimeUnit.MILLISECONDS));
            builder.sslSocketFactory(sslSocketFactory, (X509TrustManager)trustAllCerts[0]);
            builder.hostnameVerifier(new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            });

            return builder.build();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

在使用時,只需要在產生 Retrofit 時,改用這個 http client builder

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://reqres.in/")
                .addConverterFactory(GsonConverterFactory.create())
//                .client(httpClient.build())
                .client(UnsafeOkHttpClient.getUnsafeOkHttpClient())
                .build();

Service Generator

可製作 service generator,將產生 service 的程式碼再包裝起來

UserServiceGenerator.java

import okhttp3.OkHttpClient;
import okhttp3.Request;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class UserServiceGenerator {
    private static final String BASE_URL = "https://reqres.in/";

    private static Retrofit.Builder builder = new Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create());

    private static Retrofit retrofit = builder.build();

    private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder()
        .connectionPool(new ConnectionPool(10, 1000, TimeUnit.MILLISECONDS));;

    public static <S> S createService(Class<S> serviceClass) {
        return retrofit.create(serviceClass);
    }

    public static <S> S createService(Class<S> serviceClass, final String token ) {
        if ( token != null ) {
            httpClient.interceptors().clear();
            httpClient.addInterceptor( chain -> {
                Request original = chain.request();
                Request request = original.newBuilder()
                        .header("Authorization", token)
                        .build();
                return chain.proceed(request);
            });
            builder.client(httpClient.build());
            retrofit = builder.build();
        }
        return retrofit.create(serviceClass);
    }
}

使用時

UserService service = UserServiceGenerator.createService(UserService.class);

References

Retrofit

Introduction to Retrofit | Baeldung

# Retrofit 操作教學

Retrofit 2 Tutorial: Declarative REST Client for Android

Retrofit2 完全解析 探索与okhttp之间的关系_okhttp retro2-CSDN博客

2024/10/21

Java Json library: Jackson, Gson Tree Model

因為 Json 文件本身就是一個樹狀結構,處理 JSON 的 libary 都有對應可處理每一個 json property node 的工具,以下記錄如何使用 Jackson 與 Gson,對 json 做 pretty print,parsing 每個節點,新增/移除節點的方法。

pom

在 pom.xml 加上兩個 libary 的來源

        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.17.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.10.1</version>
        </dependency>

Gson

package json;
import com.google.gson.*;

public class GsonTester {
    public static void main(String args[]) {

        String jsonString =
                "{\"name\":\"John Lin\", \"age\":21,\"verified\":false,\"geo\": [100.11,90.85]}";
//        System.out.println("jsonString="+jsonString);

        // 以 JsonParser parsing 後,取得 JsonObject
        JsonObject details = JsonParser.parseString(jsonString).getAsJsonObject();

        //************/
        // pretty print json string
//            {
//                "name" : "John Lin",
//                "age" : 21,
//                "verified" : false,
//                "geo" : [
//                    100.11,
//                    90.85
//                ]
//            }
//        System.out.println(details.toString());

        System.out.println();
        System.out.println("*** original Json");
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        String jsonOutput = gson.toJson(details);
        System.out.println(jsonOutput);

        //************/
        // parsing json
        System.out.println();
        System.out.println("*** parsing Json");
        if (details.isJsonObject()) {
            // JsonElement 對應 "name":"John Lin"
            JsonElement nameNode = details.get("name");
            System.out.println("Name: " +nameNode.getAsString());

            JsonElement ageNode = details.get("age");
            System.out.println("Age: " + ageNode.getAsInt());

            JsonElement verifiedNode = details.get("verified");
            System.out.println("Verified: " + (verifiedNode.getAsBoolean() ? "Yes":"No"));

            // JsonArray 對應 [100.11,90.85]
            JsonArray geoNode = details.getAsJsonArray("geo");
            System.out.print("geo: ");
            for (int i = 0; i < geoNode.size(); i++) {
                JsonPrimitive value = geoNode.get(i).getAsJsonPrimitive();
                System.out.print(value.getAsFloat() + " ");
            }
            System.out.println();
        }

        //************/
        // add/remove property
        System.out.println();
        System.out.println("*** new Json After add/remove property");
        details.addProperty("school", "Tsing-Hua");
        details.remove("verified");
        String jsonOutput2 = gson.toJson(details);
        System.out.println(jsonOutput2);
    }
}

Jackson

package json;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonPrimitive;

public class JacksonTest {
    public static void main(String args[]) {
        try {
            String jsonString =
                    "{\"name\":\"John Lin\", \"age\":21,\"verified\":false,\"geo\": [100.11,90.85]}";
            ObjectMapper mapper = new ObjectMapper();
            JsonNode jsonObject = mapper.readTree(jsonString);

            //************/
            // pretty print json string
//            {
//                "name" : "John Lin",
//                    "age" : 21,
//                    "verified" : false,
//                    "geo" : [ 100.11, 90.85 ]
//            }
            System.out.println();
            System.out.println("*** original Json");
            String prettyJson = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject);
            System.out.println(prettyJson);

            //************/
            // parsing json
            System.out.println();
            System.out.println("*** parsing Json");
            JsonNode jsonNodeName = jsonObject.get("name");
            System.out.println("Name: " +jsonNodeName.asText() );

            JsonNode jsonNodeAge = jsonObject.get("age");
            System.out.println("Age: " + jsonNodeAge.asInt());

            JsonNode jsonNodeVerified = jsonObject.get("verified");
            System.out.println("Verified: " + (jsonNodeVerified.asBoolean() ? "Yes":"No"));

            JsonNode jsonNodeGeo = jsonObject.get("geo");
            System.out.print("geo: ");
            for (int i = 0; i < jsonNodeGeo.size(); i++) {
                double value = jsonNodeGeo.get(i).asDouble();
                System.out.print(value + " ");
            }
            System.out.println();

            //************/
            // add/remove property
            System.out.println();
            System.out.println("*** new Json After add/remove property");
            ObjectNode jsonObject2 = ((ObjectNode) jsonObject).put("school", "Tsing-Hua");
            JsonNode removedNode = jsonObject2.remove("verified" );
            String prettyJson2 = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject2);
            System.out.println(prettyJson2);

        } catch (JsonProcessingException e1) {

        }
    }
}

結果

*** original Json
{
  "name" : "John Lin",
  "age" : 21,
  "verified" : false,
  "geo" : [ 100.11, 90.85 ]
}

*** parsing Json
Name: John Lin
Age: 21
Verified: No
geo: 100.11 90.85 

*** new Json After add/remove property
{
  "name" : "John Lin",
  "age" : 21,
  "geo" : [ 100.11, 90.85 ],
  "school" : "Tsing-Hua"
}

References

Gson - Tree Model

Working with Tree Model Nodes in Jackson | Baeldung

Jackson - Marshall String to JsonNode | Baeldung

Pretty-Print a JSON in Java | Baeldung

2024/10/14

TopoJSON, GeoJSON

GeoJSON 是一種用 JSON 文件格式描述地圖的格式,2016 年 IETF 於 RFC 7946 規範了 GeoJSON 的規格。GeoJSON 的幾何物件有點:表示地理位置、線:表示街道公路邊界、多邊形:表示國家鄉鎮市界。

TopoJSON 是 GeoJSON 的擴充,TopoJSON 以一連串的點組合成的 Arcs 描述,line 與 polygon 都改用 arcs 描述,如果是邊界,在 TopoJSON 裡面的 arc 只會定義一次,這樣可有效減少文件的大小。

要將 TopoJSON 與 GeoJSON 文件互相轉換,可使用 node module

npm install topojson
npm install geojson

安裝後切換到 node_modules/topojson/node_modules/topojson-server/bin 這個目錄,可看到 geo2topo 指令

以下指令可將 GeoJSON 檔案轉換為 TopoJSON

./geo2topo towns-09007.geo.json > towns-09007.topo.json

切換到 node_modules/topojson/node_modules/topojson-client/bin 這個目錄,可看到 topo2geo 指令

這個指令可查詢 TopoJSON 裡面的地圖名稱

./topo2geo -l < towns-090007.topo.json
# towns-09007.geo

這邊會查詢到名稱為 towns-09007.geo

用以下指令將 TopoJSON 轉為 GeoJSON

./topo2geo towns-09007.geo=towns-090007-2.geo.json < towns-090007.topo.json

java jts library

以下節錄 GeoJSON 文件結構

{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "properties": {
                "id": "10005160",
                "name": "三灣鄉"
            },
            "geometry": {
                "type": "Polygon",
                "coordinates": [
                    [
                        [
                            120.97453105516638,
                            24.583295428280817
                        ],
                        [
                            120.96669830509721,
                            24.586708627549427
                        ],
                        ......
                    ]
                ]
            },
            ......
        }
    ]
}

這邊使用了兩個 library: jts, jackson,jackson 是處理 JSON 文件,jts 是處理向量圖形的 library

        <!-- https://github.com/locationtech/jts -->
        <!-- https://mvnrepository.com/artifact/org.locationtech.jts/jts-core -->
        <dependency>
            <groupId>org.locationtech.jts</groupId>
            <artifactId>jts-core</artifactId>
            <version>1.19.0</version>
        </dependency>
        <dependency>
            <groupId>org.locationtech.jts.io</groupId>
            <artifactId>jts-io-common</artifactId>
            <version>1.19.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.17.0</version>
        </dependency>

透過 Jackson,將 FeatureCollection 裡面的 features array 分開,每一個獨立去查詢,GPS 點跟 每一個 feature 的 Polygon// 測試每一個 point 跟 polygon 的關係。

相關的 methods 有這些:

  • 相等(Equals):幾何形狀拓撲上相等。

  • 脫節(Disjoint):幾何形狀沒有共有的點。

  • 相交(Intersects):幾何形狀至少有一個共有點(區別於脫節)

  • 接觸(Touches):幾何形狀有至少一個公共的邊界點,但是沒有內部點。

  • 交叉(Crosses):幾何形狀共享一些但不是所有的內部點。

  • 內含(Within):幾何形狀A的線都在幾何形狀B內部。

  • 包含(Contains):幾何形狀B的線都在幾何形狀A內部(區別於內含)

  • 重疊(Overlaps):幾何形狀共享一部分但不是所有的公共點,而且相交處有他們自己相同的區域。

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.geojson.GeoJsonReader;

import java.io.File;
import java.io.IOException;

public class PointInsidePolygon {

    public static void main(String[] args) throws IOException {
        // https://blog.csdn.net/qq_36427942/article/details/129123733
        // jackson lib to read json document
        ObjectMapper mapper = new ObjectMapper();
        ObjectNode geoJsonObject = (ObjectNode) mapper.readTree(new File("towns-10005.geo.json"));

//        System.out.println("geoJsonObject.toString()="+geoJsonObject.toString());

        // 透過 Jackson,將 FeatureCollection 裡面的 features array 分開
        // 每一個獨立去查詢,GPS 點跟 每一個 feature 的 Polygon
        // 測試每一個 point 跟 polygon 的關係
        Coordinate GPSPint = new Coordinate(120.97453105516638,24.583295428280817);
        JsonNode node2 = geoJsonObject.get("features");
//        System.out.println("node2="+node2);
        for(JsonNode node3: node2) {
//            System.out.println("node3="+node3);
            JsonNodeType node3Type = node3.getNodeType();
            JsonNode node3PropertiesNode = node3.get("properties");
            JsonNode node3PropertiesId = node3PropertiesNode.get("id");
            JsonNode node3PropertiesName = node3PropertiesNode.get("name");

            System.out.println("");
            System.out.println("node3PropertiesId="+node3PropertiesId+", node3PropertiesName="+node3PropertiesName);

            GeoJsonReader reader = new GeoJsonReader();
            Geometry geometry = null;
            try {
                geometry = reader.read(node3.toString());
            } catch (ParseException e) {
                throw new RuntimeException(e);
            }

            String geometryType = geometry.getGeometryType();
            // geometryType=GeometryCollection
            System.out.println("geometryType="+geometryType+", length="+ geometry.getLength());

//        Coordinate[] cors = geometry.getCoordinates();
//        System.out.println("cors length="+cors.length);
//        for(Coordinate c: cors) {
//            System.out.println("c ="+c.toString() );
//        }
            // get ExteriorRing
            if (geometry instanceof Polygon) {
                geometry = ((Polygon) geometry).getExteriorRing();
            } else {
                System.err.println("Invalid Polygon");
                return;
            }

            // JTS Geometry
            GeometryFactory geometryFactory = new GeometryFactory();
            Coordinate[] coordinates = geometry.getCoordinates();
            Coordinate[] jtsCoordinates = new Coordinate[coordinates.length];
            for (int i = 0; i < coordinates.length; i++) {
                jtsCoordinates[i] = new Coordinate(coordinates[i].x, coordinates[i].y);
            }
            Polygon polygon = geometryFactory.createPolygon(jtsCoordinates);

            // GPS Point
            Point gpsPoint = geometryFactory.createPoint(GPSPint);

//            相等(Equals):幾何形狀拓撲上相等。
//            脫節(Disjoint):幾何形狀沒有共有的點。
//            相交(Intersects):幾何形狀至少有一個共有點(區別於脫節)
//            接觸(Touches):幾何形狀有至少一個公共的邊界點,但是沒有內部點。
//            交叉(Crosses):幾何形狀共享一些但不是所有的內部點。
//            內含(Within):幾何形狀A的線都在幾何形狀B內部。
//            包含(Contains):幾何形狀B的線都在幾何形狀A內部(區別於內含)
//            重疊(Overlaps):幾何形狀共享一部分但不是所有的公共點,而且相交處有他們自己相同的區域。
            boolean isInside = polygon.contains(gpsPoint);
            boolean isWithin = polygon.within(gpsPoint);
            boolean intersects = polygon.intersects(gpsPoint);
            boolean overlaps = polygon.overlaps(gpsPoint);
            boolean crosses = polygon.crosses(gpsPoint);
            boolean touches = polygon.touches(gpsPoint);
            boolean disjoint = polygon.disjoint(gpsPoint);

            System.out.println("gps "+gpsPoint);
            System.out.println(" contains=" + isInside+", within=" + isWithin+", intersects="+intersects+". overlaps="+overlaps+", crosses="+crosses+", touches="+ touches+", disjoint="+disjoint);
        }
    }
}

執行結果如下:

node3PropertiesId="10005160", node3PropertiesName="三灣鄉"
geometryType=Polygon, length=0.4571892567423512
gps POINT (120.97453105516638 24.583295428280817)
 contains=false, within=false, intersects=true. overlaps=false, crosses=false, touches=true, disjoint=false

node3PropertiesId="10005110", node3PropertiesName="南庄鄉"
geometryType=Polygon, length=0.6073270143853203
gps POINT (120.97453105516638 24.583295428280817)
 contains=false, within=false, intersects=true. overlaps=false, crosses=false, touches=true, disjoint=false

node3PropertiesId="10005010", node3PropertiesName="苗栗市"
geometryType=Polygon, length=0.26286982385854196
gps POINT (120.97453105516638 24.583295428280817)
 contains=false, within=false, intersects=false. overlaps=false, crosses=false, touches=false, disjoint=true

References

D3.js應用

「GIS教程」将GeoJSON转换成TopoJSON的方法 | 麻辣GIS

GeoJSON - Wikipedia

2024/9/30

RTSP

RTSP是1996年由 RealNetworks, Netscape, 哥倫比亞大學開發,提交草案給 IETF,1998年發布為 RFC2326,2016年RTSP 2.0 發布於 RFC 7826。RTSP 是串流媒體伺服器的控制協定,可建立與控制終端設備跟伺服器之間的多媒體 session。

RTSP 協定本身看起來跟 HTTP 類似,但 HTTP 本身是 stateless,而RTSP 是 stateful,故需要追蹤 session。RTSP 是用 TCP 連線,而多媒體本身,是用 RTP 傳輸,可以用 TCP 或 UDP,常見狀況是為求傳輸速度快,使用 UDP。

節錄一個 RTSP client 取得 RTSP 的封包過程

  1. Options

    查詢 RTSP server 支援的 command

    Client -> Server

    Request: OPTIONS rtsp://192.168.1.11:8554/mystream RTSP/1.0\r\n
    CSeq: 2\r\n
    User-Agent: LibVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)\r\n
    \r\n

    Server -> Client

    Response: RTSP/1.0 200 OK\r\n
    CSeq: 2\r\n
    Public: DESCRIBE, ANNOUNCE, SETUP, PLAY, RECORD, PAUSE, GET_PARAMETER, TEARDOWN\r\n
    Server: gortsplib\r\n
    \r\n
  2. Describe

    查詢可處理的多媒體資料格式,server 以 SDP 方式回覆

    Client -> Server

    Request: DESCRIBE rtsp://192.168.1.11:8554/mystream RTSP/1.0\r\n
    CSeq: 3\r\n
    User-Agent: LibVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)\r\n
    Accept: application/sdp\r\n
    \r\n

    Server -> Client

    Response: RTSP/1.0 200 OK\r\n
    CSeq: 3\r\n
    Content-Base: rtsp://192.168.1.11:8554/mystream/\r\n
    Content-length: 560
    Content-type: application/sdp
    Server: gortsplib\r\n
    \r\n
    Session Description Protocol Version (v): 0
    Owner/Creator, Session Id (o): - 0 0 IN IP4 127.0.0.1
    Session Name (s): test
    Connection Information (c): IN IP4 0.0.0.0
    Time Description, active time (t): 0 0
    Media Description, name and address (m): video 0 RTP/AVP 96
    Media Attribute (a): control:rtsp://192.168.1.11:8554/mystream/trackID=0
    Media Attribute (a): rtpmap:96 H264/90000
    Media Attribute (a): fmtp:96 packetization-mode=1; profile-level-id=640032; sprop-parameter-sets=Z2QAMqzIUB4AiflwEQAAAwPpAAC7gA8YMZY=,aOk4XLIs
    Media Description, name and address (m): audio 0 RTP/AVP 97
    Media Attribute (a): control:rtsp://192.168.1.11:8554/mystream/trackID=1
    Media Attribute (a): rtpmap:97 mpeg4-generic/48000/2
    Media Attribute (a): fmtp:97 config=1190; indexdeltalength=3; indexlength=3; mode=AAC-hbr; profile-level-id=1; sizelength=13; streamtype=5
  3. Setup

    要求 server 設定傳送某一個 media stream,setup 要在 play 之前完成

    Client -> Server

    Request: SETUP rtsp://192.168.1.11:8554/mystream/trackID=0 RTSP/1.0\r\n
    CSeq: 4\r\n
    User-Agent: LibVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)\r\n
    Transport: RTP/AVP/TCP;unicast;interleaved=0-1
    \r\n

    Server -> Client

    Response: RTSP/1.0 200 OK\r\n
    CSeq: 4\r\n
    Server: gortsplib\r\n
    Session: 262553a7d5084e8fb57f9d8f485c89f4
    Transport: RTP/AVP/TCP;unicast;interleaved=0-1;ssrc=03DAD41B
    \r\n
  4. Setup

    client 透過 setup 跟 server 確認 stream session

    Client -> Server

    Request: SETUP rtsp://192.168.1.11:8554/mystream/trackID=1 RTSP/1.0\r\n
    CSeq: 5\r\n
    User-Agent: LibVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)\r\n
    Transport: RTP/AVP/TCP;unicast;interleaved=2-3
    Session: 262553a7d5084e8fb57f9d8f485c89f4
    \r\n

    Server -> Client

    Response: RTSP/1.0 200 OK\r\n
    CSeq: 5\r\n
    Server: gortsplib\r\n
    Session: 262553a7d5084e8fb57f9d8f485c89f4
    Transport: RTP/AVP/TCP;unicast;interleaved=2-3;ssrc=294122AE
    \r\n
  5. Play

    在 setup 設定的 session 裡面,開始播放 media

    Client -> Server

    Request: PLAY rtsp://192.168.1.11:8554/mystream/ RTSP/1.0\r\n
    CSeq: 6\r\n
    User-Agent: LibVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)\r\n
    Session: 262553a7d5084e8fb57f9d8f485c89f4
    Range: npt=0.000-\r\n
    \r\n

    Server -> Client

    Response: RTSP/1.0 200 OK\r\n
    CSeq: 6\r\n
    RTP-Info: url=rtsp://192.168.1.11:8554/mystream/trackID=0;seq=10453;rtptime=1024864644,url=rtsp://192.168.1.11:8554/mystream/trackID=1;seq=4824;rtptime=3779844790\r\n
    Server: gortsplib\r\n
    Session: 262553a7d5084e8fb57f9d8f485c89f4
    \r\n
  6. RTP

    用 RTP 的格式,傳送 media 內容

    Server -> Client

    10.. .... = Version: RFC 1889 Version (2)
    ..0. .... = Padding: False
    ...0 .... = Extension: False
    .... 0000 = Contributing source identifiers count: 0
    0... .... = Marker: False
    Payload type: DynamicRTP-Type-96 (96)
    Sequence number: 10453
    Timestamp: 1024871144
    Synchronization Source identifier: 0x03dad41b (64672795)
    Payload: 09f0
  7. Teardown

    client 通知 server 停止播放 media

    Client -> Server

    Request: TEARDOWN rtsp://192.168.1.11:8554/mystream/ RTSP/1.0\r\n
    CSeq: 7\r\n
    User-Agent: LibVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)\r\n
    Session: 262553a7d5084e8fb57f9d8f485c89f4
    \r\n

References

即時串流協定 - 維基百科,自由的百科全書

2024/9/23

Secure Reliable Transport (SRT)

Secure Reliable Transport (SRT) 是開放的 video transport protocol,由 Halvision 提出,目的是要提供一個加密、低延遲的視訊串流傳輸協定,過去比較常見的協定是 RTMP, RTSP, HLS, WebRTC。SRT 是在 2013 年由 Haivision 發表,後來 Haivision 在 2017 年將 protocol 開放,交給 SRT Alliance,然後慢慢有更多廠商支援這個協定。

在這麼長久的網路視訊串流發展歷史中,RTMP 常見於網路影片直播,尤其是行動網路的直播,RTSP 常見於網路攝影機。RTMP 是以 TCP 為基礎,因為發展當時的網路頻寬不大,必須要用 TCP 本身的連線穩定度,封包傳送機制,來確保網路直播的可用性。

SRT 則是完全使用 UDP,在 Haivision 的文件中,提出 SRT 的延遲比 RTMP 少 2.5~3.2 倍。SRT 跟 RTP 的差異是,SRT 借鑒了 RTMP 控制機制,傳輸中除了 video 資料封包,還有控制封包,控制封包可根據網路延遲及品質,動態調整發送端的 video 發送速度,也能有限制地決定要不要重傳遺失的封包。

SRT 可套用加密機制,使用最常見的 AES-128/256 加密方法。

SRT 只是一種影片切割與包裝的方法,因此能適用於任何一種影片 codec。

Server

GitHub - Edward-Wu/srt-live-server: srt live server for low latency 這是一個 SRT streaming server。

安裝前,必須要先安裝 GitHub - Haivision/srt: Secure, Reliable, Transport SRT library,我們在 CentOS 測試,根據這個文件說明,依照以下步驟安裝

sudo yum install tcl pkgconfig openssl-devel cmake gcc gcc-c++ make automake
./configure
make
make install

安裝 SRT library 後,可安裝 server,下載 srt-live-server-master.zip,解壓縮後,直接 make 即可

sudo make

執行

cd bin
./sls -c ../sls.conf

Client

測試 SRT 可使用 OBS Studio

發布的網址為

srt://192.168.1.11:8080?streamid=uplive.sls.com/live/test

接收部分,可用 VLC video player 測試,網址為

srt://192.168.1.11:8080?streamid=live.sls.com/live/test

實際上實測,OBS 發佈到 VLC 接收,大約有 4~5 秒的延遲

iOS app

可安裝 Haivision Play Pro - Haivision iOS APP,這個 app 也可以接收 SRT streaming

設定方式如下

References

什麼是SRT 安全可靠傳輸協議

Secure Reliable Transport - Wikipedia

【ProAV Lab】SRT,互聯網上的最佳視訊串流協定 | Lumens

RTMP vs. SRT: Comparing Latency and Maximum Bandwidth - Haivision