¿Cómo puedo usar ArcGIS 10.1 para encontrar un punto equidistante geodésico definido por tres puntos?

12

Por ejemplo, tengo coordenadas para tres puntos base en una costa y necesito encontrar las coordenadas del punto frente a la costa que es equidistante de los tres. Es un ejercicio simple de geometría, pero todas las mediciones deben tener en cuenta la geodesia.

Si me acercara a esto de una manera euclidiana, podría medir los caminos geodésicos que conectan los puntos base, encontrar los puntos medios de los lados del triángulo resultante y crear ortodromos perpendiculares a cada uno de esos caminos. Los tres loxodromos probablemente convergerían en el punto equidistante. Si este es el método correcto, debe haber una forma más fácil de hacerlo en Arc.

Necesito encontrar O

Grasa
fuente
¿Hay restricciones en las posiciones relativas de los 3 puntos? Imagen de la costa este, el punto medio está más al este. Su solución no funcionaría ya que las perpendiculares no convergerían en alta mar. ¡Estoy seguro de que podemos encontrar otros casos malos!
mkennedy
Me pregunto si podría usar una proyección de preservación de distancia y ejecutar el cálculo desde allí. progonos.com/furuti/MapProj/Normal/CartProp/DistPres/… No estoy seguro del algoritmo para hacerlo, debe haber uno ... tal vez sea el baricentro: en.wikipedia.org/wiki/Barycentric_coordinate_system
Alex Leith
Para encontrar soluciones a un problema estrechamente relacionado, busque en nuestro sitio "trilateración" . Además, gis.stackexchange.com/questions/10332/… es un duplicado pero no tiene respuestas adecuadas (muy probablemente porque la pregunta se hizo de manera confusa).
whuber
@mkennedy En principio, no hay casos malos, solo numéricamente inestables. Esto ocurre cuando los tres puntos base son colineales; las dos soluciones (en un modelo esférico) ocurren en los dos polos de la geodésica común; en un modelo elipsoidal ocurren cerca de donde se esperarían esos polos.
Whuber
El uso de loxodromes aquí no sería correcto: no son las bisectrices perpendiculares. En la esfera, estas líneas serán partes de grandes círculos (geodésicas), pero en el elipsoide, se apartarán un poco de ser geodésicas.
whuber

Respuestas:

10

Esta respuesta se divide en múltiples secciones:

  • Análisis y reducción del problema , que muestra cómo encontrar el punto deseado con rutinas "enlatadas".

  • Ilustración: un prototipo de trabajo , dando un código de trabajo.

  • Ejemplo , que muestra ejemplos de las soluciones.

  • Dificultades , discutir posibles problemas y cómo hacer frente a ellos.

  • Implementación de ArcGIS , comentarios sobre la creación de una herramienta ArcGIS personalizada y dónde obtener las rutinas necesarias.


Análisis y reducción del problema

Comencemos por observar que en el modelo esférico (perfectamente redonda) habrá siempre ser una solución --en realidad, exactamente dos soluciones. Dados los puntos base A, B y C, cada par determina su "bisectriz perpendicular", que es el conjunto de puntos equidistantes de los dos puntos dados. Esta bisectriz es una geodésica (gran círculo). La geometría esférica es elíptica : cualquiera de las dos geodésicas se cruzan (en dos puntos únicos). Por lo tanto, los puntos de intersección de la bisectriz de AB y la bisectriz de BC son, por definición, equidistantes de A, B y C, resolviendo así el problema. (Vea la primera figura a continuación).

Las cosas se ven más complicadas en un elipsoide, pero debido a que es una pequeña perturbación de la esfera, podemos esperar un comportamiento similar. (El análisis de esto nos llevaría demasiado lejos). Sin embargo, las complicadas fórmulas utilizadas (internamente dentro de un SIG) para calcular distancias precisas en un elipsoide no son una complicación conceptual: el problema es básicamente el mismo. Para ver cuán simple es realmente el problema, vamos a exponerlo de manera algo abstracta. En esta declaración, "d (U, V)" se refiere a la distancia verdadera y totalmente precisa entre los puntos U y V.

Dados tres puntos A, B, C (como pares lat-lon) en un elipsoide, encuentre un punto X para el cual (1) d (X, A) = d (X, B) = d (X, C) y ( 2) esta distancia común es lo más pequeña posible.

Estas tres distancias dependen de la X desconocida . Por lo tanto, las diferencias en distancias u (X) = d (X, A) - d (X, B) yv (X) = d (X, B) - d (X, C) son funciones de valor real de X. De nuevo, de manera algo abstracta, podemos agrupar estas diferencias en un par ordenado. También usaremos (lat, lon) como coordenadas para X, lo que nos permite considerarlo también como un par ordenado, digamos X = (phi, lambda). En esta configuración, la función

F (phi, lambda) = (u (X), v (X))

es una función de una porción de un espacio bidimensional que toma valores en un espacio bidimensional y nuestro problema se reduce a

Encuentre todos los posibles (phi, lambda) para los cuales F (phi, lambda) = (0,0).

Aquí es donde vale la pena la abstracción: existe un gran software excelente para resolver este problema (puramente numérico, multidimensional, búsqueda de raíces). La forma en que funciona es que escribe una rutina para calcular F , luego la pasa al software junto con cualquier información sobre restricciones en su entrada ( phi debe estar entre -90 y 90 grados y lambda debe estar entre -180 y 180 grados). Se arranca por una fracción de segundo y devuelve (típicamente) solo un valor de ( phi , lambda ), si puede encontrar uno.

Hay detalles que manejar, porque hay un arte en esto: hay varios métodos de solución para elegir, dependiendo de cómo F "se comporta"; ayuda a "dirigir" el software dándole un punto de partida razonable para su búsqueda (esta es una forma en que podemos obtener la solución más cercana , en lugar de cualquier otra); y generalmente necesita especificar qué tan precisa le gustaría que fuera la solución (para que sepa cuándo detener la búsqueda). (Para obtener más información sobre lo que los analistas de SIG necesitan saber sobre estos detalles, que surgen mucho en los problemas de SIG, visite Recomendar temas para que se incluyan en un curso de Ciencias de la Computación para Tecnologías Geoespaciales y busque en la sección "Miscelánea" al final. )


Ilustración: un prototipo de trabajo

El análisis muestra que necesitamos programar dos cosas: una estimación inicial cruda de la solución y el cálculo de F en sí.

La estimación inicial se puede hacer mediante un "promedio esférico" de los tres puntos base. Esto se obtiene al representarlos en coordenadas cartesianas geocéntricas (x, y, z), promediando esas coordenadas, y proyectando ese promedio de regreso a la esfera y reexpresándolo en latitud y longitud. El tamaño de la esfera es irrelevante y, por lo tanto, los cálculos se hacen directos: dado que este es solo un punto de partida, no necesitamos cálculos elipsoidales.

Para este prototipo de trabajo usé Mathematica 8.

sphericalMean[points_] := Module[{sToC, cToS, cMean},
  sToC[{f_, l_}] := {Cos[f] Cos[l], Cos[f] Sin[l], Sin[f]};
  cToS[{x_, y_, z_}] := {ArcTan[x, y], ArcTan[Norm[{x, y}], z]};
  cMean = Mean[sToC /@ (points Degree)];
  If[Norm[Most@cMean] < 10^(-8), Mean[points], cToS[cMean]] / Degree
  ]

(La Ifcondición final comprueba si el promedio puede fallar claramente para indicar una longitud; si es así, vuelve a una media aritmética recta de las latitudes y longitudes de su entrada; tal vez no sea una gran opción, pero al menos una válida. Para aquellos que usan este código como guía de implementación, tenga en cuenta que los argumentos de Mathematica ArcTan se invierten en comparación con la mayoría de las otras implementaciones: su primer argumento es la coordenada x, su segundo es la coordenada y, y devuelve el ángulo formado por el vector ( x, y).)

En cuanto a la segunda parte, debido a que Mathematica , como ArcGIS y casi todos los demás SIG, contiene código para calcular distancias precisas en el elipsoide, no hay casi nada que escribir. Simplemente llamamos a la rutina de búsqueda de raíz:

tri[a_, b_, c_] := Block[{d = sphericalMean[{a, b, c}], sol, f, q},
   sol = FindRoot[{GeoDistance[{Mod[f, 180, -90], Mod[q, 360, -180]}, a] == 
                   GeoDistance[{Mod[f, 180, -90], Mod[q, 360, -180]}, b] ==
                   GeoDistance[{Mod[f, 180, -90], Mod[q, 360, -180]}, c]}, 
           {{f, d[[1]]}, {q, d[[2]]}}, 
           MaxIterations -> 1000, AccuracyGoal -> Infinity, PrecisionGoal -> 8];
   {Mod[f, 180, -90], Mod[q, 360, -180]} /. sol
   ];

El aspecto más notable de esta implementación es cómo esquiva la necesidad de restringir la latitud ( f) y la longitud ( q) calculando siempre el módulo 180 y 360 grados, respectivamente. Esto evita tener que restringir el problema (que a menudo crea complicaciones). Los parámetros de control, MaxIterationsetc., se modifican para que este código proporcione la mayor precisión posible.

Para verlo en acción, apliquémoslo a los tres puntos básicos dados en una pregunta relacionada :

sol = tri @@ (bases = {{-6.28530175, 106.9004975375}, {-6.28955287, 106.89573839}, {-6.28388865789474, 106.908087643421}})

{-6.29692, 106.907}

Las distancias calculadas entre esta solución y los tres puntos son

{1450.23206979, 1450.23206979, 1450.23206978}

(estos son metros). Están de acuerdo con el undécimo dígito significativo (que es demasiado preciso, en realidad, ya que las distancias rara vez son precisas mejor que un milímetro más o menos). Aquí hay una imagen de estos tres puntos (negro), sus tres bisectrices mutuas y la solución (rojo):

Figura 1


Ejemplo

Para probar esta implementación y obtener una mejor comprensión de cómo se comporta el problema, aquí hay una gráfica de contorno de la discrepancia cuadrática media en las distancias para tres puntos base ampliamente espaciados. (La discrepancia RMS se obtiene calculando las tres diferencias d (X, A) -d (X, B), d (X, B) -d (X, C) yd (X, C) -d (X , A), promediando sus cuadrados y sacando la raíz cuadrada. Es igual a cero cuando X resuelve el problema y, de lo contrario, aumenta a medida que X se aleja de una solución, y así mide cuán "cercanos" estamos para ser una solución en cualquier ubicación. )

Figura 2

Los puntos base (60, -120), (10, -40) y (45,10) se muestran en rojo en esta proyección de Plate Carree; la solución (49.2644488, -49.9052992), que requirió 0.03 segundos para calcular, está en amarillo. Su discrepancia RMS es inferior a tres nanómetros , a pesar de que todas las distancias relevantes son miles de kilómetros. Las áreas oscuras muestran valores pequeños del RMS y las áreas claras muestran valores altos.

Este mapa muestra claramente que hay otra solución cerca (-49.2018206, 130.0297177) (calculada a un RMS de dos nanómetros al establecer el valor de búsqueda inicial diametralmente opuesto a la primera solución).


Trampas

Inestabilidad numérica

Cuando los puntos base están casi colineales y muy juntos, todas las soluciones estarán a casi medio mundo de distancia y serán extremadamente difíciles de precisar con precisión. La razón es que pequeños cambios en una ubicación en todo el mundo, moviéndolo hacia o desde los puntos base, inducirán solo cambios increíblemente pequeños en las diferencias de distancias. Simplemente no hay suficiente precisión y precisión incorporada en el cálculo habitual de distancias geodésicas para precisar el resultado.

Por ejemplo, comenzando con los puntos base en (45.001, 0), (45, 0) y (44.999,0), que están separados a lo largo del meridiano principal por solo 111 metros entre cada par, triobtiene la solución (11.8213, 77.745 ) Las distancias desde él a los puntos base son 8,127,964.998 77; 8.127.964.998 41; y 8,127,964.998 65 metros, respectivamente. ¡Están de acuerdo con el milímetro más cercano! No estoy seguro de cuán preciso puede ser este resultado, pero no me sorprendería en lo más mínimo si otras implementaciones devolvieran ubicaciones lejos de esta, mostrando casi la misma igualdad de las tres distancias.

Tiempo de cómputo

Estos cálculos, debido a que implican una búsqueda considerable utilizando cálculos de distancia complicados, no son rápidos, por lo general requieren una fracción notable de un segundo. Las aplicaciones en tiempo real deben ser conscientes de esto.


Implementación de ArcGIS

Python es el entorno de secuencias de comandos preferido para ArcGIS (a partir de la versión 9). El paquete scipy.optimize tiene un buscador de raíz multivariado rootque debe hacer lo que FindRoothace en el código de Mathematica . Por supuesto, ArcGIS ofrece cálculos precisos de distancia elipsoidal. El resto, entonces, son todos los detalles de implementación: decida cómo se obtendrán las coordenadas del punto base (de una capa, escrita por el usuario, de un archivo de texto, del mouse) y cómo se presentará la salida (como coordenadas visualizado en la pantalla? como un punto gráfico? como un nuevo objeto de punto en una capa?), escriba esa interfaz, conecte el código de Mathematica que se muestra aquí (sencillo), y estará listo.

whuber
fuente
3
+1 Muy minucioso. Creo que deberías comenzar a cobrar por esto, @whuber.
Radar
2
@ Radar Gracias. Espero que la gente compre mi libro cuando (alguna vez) finalmente aparezca :-).
whuber
1
Will Bill ... ¡Envía un borrador!
¡Excelente! Aún así, parece que una solución analítica sería posible. Al restablecer el problema en el espacio cartesiano 3d con 3 puntos A, B, C y E donde E es el centro de la tierra. Luego encuentre dos planos Plane1 y Plane2. Plane1 sería el plano que es normal a planeABE y que pasa por E, punto medio (A, B). Del mismo modo, Plane2 sería el plano normal a planeACE y pasaría por E, punto medio (C, E). La línea O formada por la intersección de Plane1 y Plane2 representa puntos equidistantes a los 3 puntos. El más cercano de los dos puntos a A (o B o C) donde la líneaO interseca la esfera es el puntoO.
Kirk Kuykendall
Esa solución analítica, @Kirk, se aplica solo a la esfera. (Las intersecciones de planos con el elipsoide nunca son bisectrices perpendiculares en la métrica del elipsoide, excepto por algunos casos excepcionales obvios: cuando son meridianos o el ecuador.)
whuber
3

Como observa, este problema surge al determinar los límites marítimos; a menudo se le conoce como el problema de "tres puntos" y puede buscarlo en Google y encontrar varios documentos que lo aborden. Uno de estos documentos es mío (!) Y ofrezco una solución precisa y rápidamente convergente. Consulte la Sección 14 de http://arxiv.org/abs/1102.1215

El método consta de los siguientes pasos:

  1. adivina un punto triple O
  2. use O como el centro de una proyección equidistante azimutal
  3. proyecto A, B, C, a esta proyección
  4. encuentra el punto triple en esta proyección, O '
  5. use O 'como el nuevo centro de proyección
  6. repite hasta que O 'y O coincidan

La fórmula necesaria para la solución de tres puntos en la proyección se da en el documento. Mientras utilice una proyección equidistante azimutal precisa, la respuesta será exacta. La convergencia es cuadrática, lo que significa que solo se necesitan unas pocas iteraciones. Esto seguramente superará a los métodos generales de búsqueda de raíces sugeridos por @whuber.

No puedo ayudarte directamente con ArcGIS. Puede obtener mi paquete de Python para hacer cálculos geodésicos desde https://pypi.python.org/pypi/geographiclib y codificar la proyección en función de esto es simple.


Editar

Cayley consideró el problema de encontrar el punto triple en el caso degenerado de @ whuber (45 + eps, 0) (45,0) (45-eps, 0) en En las líneas geodésicas de un esferoide achatado , Phil. revista (1870), http://books.google.com/books?id=4XGIOoCMYYAC&pg=PA15

En este caso, el punto triple se obtiene siguiendo una geodésica de (45,0) con azimut 90 y encontrando el punto en el que se desvanece la escala geodésica. Para el elipsoide WGS84, este punto es (-0.10690908732248, 89.89291072793167). La distancia desde este punto a cada uno de (45.001,0), (45,0), (44.999) es 10010287.665788943 m (dentro de un nanómetro más o menos). Esto es aproximadamente 1882 km más que la estimación de Whuber (lo que solo muestra cuán inestable es este caso). Para una tierra esférica, el punto triple sería (0,90) o (0, -90), por supuesto.

ADENDA: Aquí hay una implementación del método equidistante azimutal usando Matlab

function [lat, lon] = tripoint(lat1, lon1, lat2, lon2, lat3, lon3)
% Compute point equidistant from arguments
% Requires:
%   http://www.mathworks.com/matlabcentral/fileexchange/39108
%   http://www.mathworks.com/matlabcentral/fileexchange/39366
  lats = [lat1, lat2, lat3];
  lons = [lon1, lon2, lon3];
  lat0 = lat1;  lon0 = lon1; % feeble guess for tri point
  for i = 1:6
    [x, y] = eqdazim_fwd(lat0, lon0, lats, lons);
    a = [x(1), y(1), 0];
    b = [x(2), y(2), 0];
    c = [x(3), y(3), 0];
    z = [0, 0, 1];
    % Eq. (97) of http://arxiv.org/abs/1102.1215
    o = cross((a*a') * (b - c) + (b*b') * (c - a) + (c*c') * (a - b), z) ...
        / (2 * dot(cross(a - b, b - c), z));
    [lat0, lon0] = eqdazim_inv(lat0, lon0, o(1), o(2))
  end
  % optional check
  s12 = geoddistance(lat0, lon0, lats, lons); ds12 = max(s12) - min(s12)
  lat = lat0; lon = lon0;
end

Probar esto usando Octave me sale

octava: 1> formato largo
octava: 2> [lat0, lon0] = tripoint (41, -74,36,140, ​​-41,175)
lat0 = 15.4151378380375
lon0 = -162.479314381144
lat0 = 15.9969703299812
lon0 = -147.046790722192
lat0 = 16.2232960167545
lon0 = -147.157646039471
lat0 = 16.2233394851560
lon0 = -147.157748279290
lat0 = 16.2233394851809
lon0 = -147.157748279312
lat0 = 16.2233394851809
lon0 = -147.157748279312
ds12 = 3.72529029846191e-09
lat0 = 16.2233394851809
lon0 = -147.157748279312

como el punto triple para Nueva York, Tokio y Wellington.

Este método es inexacto para los puntos colineales vecinos, por ejemplo, [45.001,0], [45,0], [44.999,0]. En ese caso, debe resolver para M 12 = 0 en una geodésica que emana de [45,0] en el acimut 90. La siguiente función hace el truco (usando el método de Newton):

function [lat2,lon2] = semiconj(lat1, lon1, azi1)
% Find the point where neighboring parallel geodesics emanating from
% close to [lat1, lon1] with azimuth azi1 intersect.

  % First guess is 90 deg on aux sphere
  [lat2, lon2, ~, ~, m12, M12, M21, s12] = ...
      geodreckon(lat1, lon1, 90, azi1, defaultellipsoid(), true);
  M12
  % dM12/ds2 = - (1 - M12*M21/m12)
  for i = 1:3
    s12 = s12 - M12 / ( -(1 - M12*M21)/m12 ); % Newton
    [lat2, lon2, ~, ~, m12, M12, M21] = geodreckon(lat1, lon1, s12, azi1);
    M12
  end
end

Por ejemplo, esto da:

[lat2, lon2] = semiconj (45, 0, 90)
M12 = 0.00262997817649321
M12 = -6.08402492665097e-09
M12 = 4.38017677684144e-17
M12 = 4.38017677684144e-17
lat2 = -0.106909087322479
lon2 = 89.8929107279317
cffk
fuente
+1. Sin embargo, no está claro que un buscador raíz general funcione menos bien: la función se comporta muy bien cerca de su óptimo y el método de Newton, por ejemplo, también convergerá cuadráticamente. ( Mathematica generalmente está tomando alrededor de cuatro pasos para converger; cada paso requiere cuatro evaluaciones para estimar el jacobiano). La ventaja real que veo para su método es que se puede escribir fácilmente en un SIG sin recurrir al uso de un buscador de raíz.
whuber
Estoy de acuerdo. Mi método es equivalente a Newton y, por lo tanto, en contraste con el método de búsqueda de raíz de Mathematica, no hay necesidad de estimar el gradiente tomando diferencias.
cffk
Correcto, pero tienes que hacer la reproyección cada vez, lo que parece que es casi la misma cantidad de trabajo. Sin embargo, aprecio la simplicidad y elegancia de su enfoque: es obvio de inmediato que debería funcionar y convergerá rápidamente.
whuber
He publicado resultados para los mismos puntos de prueba en mi respuesta.
Kirk Kuykendall
3

Tenía curiosidad por ver qué tan rápido el enfoque de @ cffk converge en una solución, así que escribí una prueba usando arcobjects, que produjo esta salida. Las distancias son en metros:

0 longitude: 0 latitude: 90
    Distances: 3134.05443974188 2844.67237777542 3234.33025754997
    Diffs: 289.382061966458 -389.657879774548 -100.27581780809
1 longitude: 106.906152157596 latitude: -6.31307123035178
    Distances: 1450.23208989615 1450.23208089398 1450.23209429293
    Diffs: 9.00216559784894E-06 -1.33989510686661E-05 -4.39678547081712E-06
2 longitude: 106.906583669013 latitude: -6.29691590176649
    Distances: 1450.23206976414 1450.23206976408 1450.23206976433
    Diffs: 6.18456397205591E-11 -2.47382558882236E-10 -1.85536919161677E-10
3 longitude: 106.906583669041 latitude: -6.29691590154641
    Distances: 1450.23206976438 1450.23206976423 1450.23206976459
    Diffs: 1.47565515362658E-10 -3.61751517630182E-10 -2.14186002267525E-10
4 longitude: 106.906583669041 latitude: -6.29691590154641
    Distances: 1450.23206976438 1450.23206976423 1450.23206976459
    Diffs: 1.47565515362658E-10 -3.61751517630182E-10 -2.14186002267525E-10

Aquí está el código fuente. (Editar) Cambió FindCircleCenter para manejar intersecciones (puntos centrales) que caen del borde de la proyección azimutal:

public static void Test()
{
    var t = Type.GetTypeFromProgID("esriGeometry.SpatialReferenceEnvironment");
    var srf = Activator.CreateInstance(t) as ISpatialReferenceFactory2;
    var pcs = srf.CreateProjectedCoordinateSystem((int)esriSRProjCSType.esriSRProjCS_WGS1984N_PoleAziEqui)
        as IProjectedCoordinateSystem2;

    var pntA = MakePoint(106.9004975375, -6.28530175, pcs.GeographicCoordinateSystem);
    var pntB = MakePoint(106.89573839, -6.28955287, pcs.GeographicCoordinateSystem);
    var pntC = MakePoint(106.908087643421, -6.28388865789474, pcs.GeographicCoordinateSystem);

    int maxIter = 5;
    for (int i = 0; i < maxIter; i++)
    {
        var msg = string.Format("{0} longitude: {1} latitude: {2}", i, pcs.get_CentralMeridian(true), pcs.LatitudeOfOrigin);
        Debug.Print(msg);
        var newCenter = FindCircleCenter(ProjectClone(pntA, pcs), ProjectClone(pntB, pcs), ProjectClone(pntC, pcs));
        newCenter.Project(pcs.GeographicCoordinateSystem); // unproject
        var distA = GetGeodesicDistance(newCenter, pntA);
        var distB = GetGeodesicDistance(newCenter, pntB);
        var distC = GetGeodesicDistance(newCenter, pntC);
        Debug.Print("\tDistances: {0} {1} {2}", distA, distB, distC);
        var diffAB = distA - distB;
        var diffBC = distB - distC;
        var diffAC = distA - distC;
        Debug.Print("\tDiffs: {0} {1} {2}", diffAB, diffBC, diffAC);

        pcs.set_CentralMeridian(true, newCenter.X);
        pcs.LatitudeOfOrigin = newCenter.Y;
    }
}
public static IPoint FindCircleCenter(IPoint a, IPoint b, IPoint c)
{
    // from http://blog.csharphelper.com/2011/11/08/draw-a-circle-through-three-points-in-c.aspx
    // Get the perpendicular bisector of (x1, y1) and (x2, y2).
    var x1 = (b.X + a.X) / 2;
    var y1 = (b.Y + a.Y) / 2;
    var dy1 = b.X - a.X;
    var dx1 = -(b.Y - a.Y);

    // Get the perpendicular bisector of (x2, y2) and (x3, y3).
    var x2 = (c.X + b.X) / 2;
    var y2 = (c.Y + b.Y) / 2;
    var dy2 = c.X - b.X;
    var dx2 = -(c.Y - b.Y);

    // See where the lines intersect.
    var cx = (y1 * dx1 * dx2 + x2 * dx1 * dy2 - x1 * dy1 * dx2 - y2 * dx1 * dx2)
        / (dx1 * dy2 - dy1 * dx2);
    var cy = (cx - x1) * dy1 / dx1 + y1;

    // make sure the intersection point falls
    // within the projection.
    var earthRadius = ((IProjectedCoordinateSystem)a.SpatialReference).GeographicCoordinateSystem.Datum.Spheroid.SemiMinorAxis;

    // distance is from center of projection
    var dist = Math.Sqrt((cx * cx) + (cy * cy));
    double factor = 1.0;
    if (dist > earthRadius * Math.PI)
    {
        // apply a factor so we don't fall off the edge
        // of the projection
        factor = earthRadius / dist;
    }
    var outPoint = new PointClass() as IPoint;
    outPoint.PutCoords(cx * factor, cy* factor);
    outPoint.SpatialReference = a.SpatialReference;
    return outPoint;
}

public static double GetGeodesicDistance(IPoint pnt1, IPoint pnt2)
{
    var pc = new PolylineClass() as IPointCollection;
    var gcs = pnt1.SpatialReference as IGeographicCoordinateSystem;
    if (gcs == null)
        throw new Exception("point does not have a gcs");
    ((IGeometry)pc).SpatialReference = gcs;
    pc.AddPoint(pnt1);
    pc.AddPoint(pnt2);
    var t = Type.GetTypeFromProgID("esriGeometry.SpatialReferenceEnvironment");
    var srf = Activator.CreateInstance(t) as ISpatialReferenceFactory2;
    var unit = srf.CreateUnit((int)esriSRUnitType.esriSRUnit_Meter) as ILinearUnit;
    var pcGeodetic = pc as IPolycurveGeodetic;
    return pcGeodetic.get_LengthGeodetic(esriGeodeticType.esriGeodeticTypeGeodesic, unit);
}

public static IPoint ProjectClone(IPoint pnt, ISpatialReference sr)
{
    var clone = ((IClone)pnt).Clone() as IPoint;
    clone.Project(sr);
    return clone;
}

public static IPoint MakePoint(double longitude, double latitude, ISpatialReference sr)
{
    var pnt = new PointClass() as IPoint;
    pnt.PutCoords(longitude, latitude);
    pnt.SpatialReference = sr;
    return pnt;
}

También hay un enfoque alternativo en la edición de junio de 2013 de MSDN Magazine, Amoeba Method Optimization usando C # .


Editar

El código publicado anteriormente convergió a la antípoda en algunos casos. He alterado el código para que produzca esta salida para los puntos de prueba de @ cffk.

Aquí está la salida que ahora produce:

0 0
0 longitude: 0 latitude: 0
    MaxDiff: 1859074.90170379 Distances: 13541157.6493561 11682082.7476523 11863320.2116807
1 longitude: 43.5318402621384 latitude: -17.1167429904981
    MaxDiff: 21796.9793742411 Distances: 12584188.7592282 12588146.4851222 12566349.505748
2 longitude: 32.8331167578493 latitude: -16.2707976739314
    MaxDiff: 6.05585224926472 Distances: 12577536.3369782 12577541.3560203 12577542.3928305
3 longitude: 32.8623898057665 latitude: -16.1374156408507
    MaxDiff: 5.58793544769287E-07 Distances: 12577539.6118671 12577539.6118666 12577539.6118669
4 longitude: -147.137582018133 latitude: 16.1374288796667
    MaxDiff: 1.12284109462053 Distances: 7441375.08265703 7441376.12671342 7441376.20549812
5 longitude: -147.157742373074 latitude: 16.2233413614432
    MaxDiff: 7.45058059692383E-09 Distances: 7441375.70752843 7441375.70752842 7441375.70752842
5 longitude: -147.157742373074 latitude: 16.2233413614432 Distance 7441375.70752843
iterations: 5

Aquí está el código revisado:

class Program
{
    private static LicenseInitializer m_AOLicenseInitializer = new tripoint.LicenseInitializer();

    [STAThread()]
    static void Main(string[] args)
    {
        //ESRI License Initializer generated code.
        m_AOLicenseInitializer.InitializeApplication(new esriLicenseProductCode[] { esriLicenseProductCode.esriLicenseProductCodeStandard },
        new esriLicenseExtensionCode[] { });
        try
        {
            var t = Type.GetTypeFromProgID("esriGeometry.SpatialReferenceEnvironment");
            var srf = Activator.CreateInstance(t) as ISpatialReferenceFactory2;
            var pcs = srf.CreateProjectedCoordinateSystem((int)esriSRProjCSType.esriSRProjCS_World_AzimuthalEquidistant)
                as IProjectedCoordinateSystem2;
            Debug.Print("{0} {1}", pcs.get_CentralMeridian(true), pcs.LatitudeOfOrigin);
            int max = int.MinValue;
            for (int i = 0; i < 1; i++)
            {
                var iterations = Test(pcs);
                max = Math.Max(max, iterations);
                Debug.Print("iterations: {0}", iterations);
            }
            Debug.Print("max number of iterations: {0}", max);
        }
        catch (Exception ex)
        {
            Debug.Print(ex.Message);
            Debug.Print(ex.StackTrace);
        }
        //ESRI License Initializer generated code.
        //Do not make any call to ArcObjects after ShutDownApplication()
        m_AOLicenseInitializer.ShutdownApplication();
    }
    public static int Test(IProjectedCoordinateSystem2 pcs)
    {
        var pntA = MakePoint(-74.0, 41.0, pcs.GeographicCoordinateSystem);
        var pntB = MakePoint(140.0, 36.0, pcs.GeographicCoordinateSystem);
        var pntC = MakePoint(175.0, -41.0, pcs.GeographicCoordinateSystem);


        //var r = new Random();
        //var pntA = MakeRandomPoint(r, pcs.GeographicCoordinateSystem);
        //var pntB = MakeRandomPoint(r, pcs.GeographicCoordinateSystem);
        //var pntC = MakeRandomPoint(r, pcs.GeographicCoordinateSystem);

        int maxIterations = 100;
        for (int i = 0; i < maxIterations; i++)
        {
            var msg = string.Format("{0} longitude: {1} latitude: {2}", i, pcs.get_CentralMeridian(true), pcs.LatitudeOfOrigin);
            Debug.Print(msg);
            var newCenter = FindCircleCenter(ProjectClone(pntA, pcs), ProjectClone(pntB, pcs), ProjectClone(pntC, pcs));
            var c = ((IClone)newCenter).Clone() as IPoint;
            newCenter.Project(pcs.GeographicCoordinateSystem); // unproject
            //newCenter = MakePoint(-147.1577482, 16.2233394, pcs.GeographicCoordinateSystem);
            var distA = GetGeodesicDistance(newCenter, pntA);
            var distB = GetGeodesicDistance(newCenter, pntB);
            var distC = GetGeodesicDistance(newCenter, pntC);
            var diffAB = Math.Abs(distA - distB);
            var diffBC = Math.Abs(distB - distC);
            var diffAC = Math.Abs(distA - distC);
            var maxDiff = GetMax(new double[] {diffAB,diffAC,diffBC});
            Debug.Print("\tMaxDiff: {0} Distances: {1} {2} {3}",maxDiff, distA, distB, distC);
            if (maxDiff < 0.000001)
            {
                var earthRadius = pcs.GeographicCoordinateSystem.Datum.Spheroid.SemiMinorAxis;
                if (distA > earthRadius * Math.PI / 2.0)
                {
                    newCenter = AntiPode(newCenter);
                }
                else
                {
                    Debug.Print("{0} longitude: {1} latitude: {2} Distance {3}", i, pcs.get_CentralMeridian(true), pcs.LatitudeOfOrigin, distA);
                    return i;
                }
            }
            //Debug.Print("\tDiffs: {0} {1} {2}", diffAB, diffBC, diffAC);

            pcs.set_CentralMeridian(true, newCenter.X);
            pcs.LatitudeOfOrigin = newCenter.Y;
        }
        return maxIterations;
    }

    public static IPoint FindCircleCenter(IPoint a, IPoint b, IPoint c)
    {
        // from http://blog.csharphelper.com/2011/11/08/draw-a-circle-through-three-points-in-c.aspx
        // Get the perpendicular bisector of (x1, y1) and (x2, y2).
        var x1 = (b.X + a.X) / 2;
        var y1 = (b.Y + a.Y) / 2;
        var dy1 = b.X - a.X;
        var dx1 = -(b.Y - a.Y);

        // Get the perpendicular bisector of (x2, y2) and (x3, y3).
        var x2 = (c.X + b.X) / 2;
        var y2 = (c.Y + b.Y) / 2;
        var dy2 = c.X - b.X;
        var dx2 = -(c.Y - b.Y);

        // See where the lines intersect.
        var cx = (y1 * dx1 * dx2 + x2 * dx1 * dy2 - x1 * dy1 * dx2 - y2 * dx1 * dx2)
            / (dx1 * dy2 - dy1 * dx2);
        var cy = (cx - x1) * dy1 / dx1 + y1;

        // make sure the intersection point falls
        // within the projection.
        var earthRadius = ((IProjectedCoordinateSystem)a.SpatialReference).GeographicCoordinateSystem.Datum.Spheroid.SemiMinorAxis;

        // distance is from center of projection
        var dist = Math.Sqrt((cx * cx) + (cy * cy));
        double factor = 1.0;
        if (dist > earthRadius * Math.PI)
        {
            // apply a factor so we don't fall off the edge
            // of the projection
            factor = earthRadius / dist;
        }
        var outPoint = new PointClass() as IPoint;
        outPoint.PutCoords(cx * factor, cy* factor);
        outPoint.SpatialReference = a.SpatialReference;
        return outPoint;
    }

    public static IPoint AntiPode(IPoint pnt)
    {
        if (!(pnt.SpatialReference is IGeographicCoordinateSystem))
            throw new Exception("antipode of non-gcs projection not supported");
        var outPnt = new PointClass() as IPoint;
        outPnt.SpatialReference = pnt.SpatialReference;
        if (pnt.X < 0.0)
            outPnt.X = 180.0 + pnt.X;
        else
            outPnt.X = pnt.X - 180.0;
        outPnt.Y = -pnt.Y;
        return outPnt;
    }

    public static IPoint MakeRandomPoint(Random r, IGeographicCoordinateSystem gcs)
    {
        var latitude = (r.NextDouble() - 0.5) * 180.0;
        var longitude = (r.NextDouble() - 0.5) * 360.0;
        //Debug.Print("{0} {1}", latitude, longitude);
        return MakePoint(longitude, latitude, gcs);
    }
    public static double GetMax(double[] dbls)
    {
        var max = double.MinValue;
        foreach (var d in dbls)
        {
            if (d > max)
                max = d;
        }
        return max;
    }
    public static IPoint MakePoint(IPoint[] pnts)
    {
        double sumx = 0.0;
        double sumy = 0.0;
        foreach (var pnt in pnts)
        {
            sumx += pnt.X;
            sumy += pnt.Y;
        }
        return MakePoint(sumx / pnts.Length, sumy / pnts.Length, pnts[0].SpatialReference);
    }
    public static double GetGeodesicDistance(IPoint pnt1, IPoint pnt2)
    {
        var pc = new PolylineClass() as IPointCollection;
        var gcs = pnt1.SpatialReference as IGeographicCoordinateSystem;
        if (gcs == null)
            throw new Exception("point does not have a gcs");
        ((IGeometry)pc).SpatialReference = gcs;
        pc.AddPoint(pnt1);
        pc.AddPoint(pnt2);
        var t = Type.GetTypeFromProgID("esriGeometry.SpatialReferenceEnvironment");
        var srf = Activator.CreateInstance(t) as ISpatialReferenceFactory2;
        var unit = srf.CreateUnit((int)esriSRUnitType.esriSRUnit_Meter) as ILinearUnit;
        var pcGeodetic = pc as IPolycurveGeodetic;
        return pcGeodetic.get_LengthGeodetic(esriGeodeticType.esriGeodeticTypeGeodesic, unit);
    }

    public static IPoint ProjectClone(IPoint pnt, ISpatialReference sr)
    {
        var clone = ((IClone)pnt).Clone() as IPoint;
        clone.Project(sr);
        return clone;
    }

    public static IPoint MakePoint(double longitude, double latitude, ISpatialReference sr)
    {
        var pnt = new PointClass() as IPoint;
        pnt.PutCoords(longitude, latitude);
        pnt.SpatialReference = sr;
        return pnt;
    }
}

Editar

Aquí están los resultados que obtengo con esriSRProjCS_WGS1984N_PoleAziEqui

0 90
0 longitude: 0 latitude: 90
    MaxDiff: 1275775.91880553 Distances: 8003451.67666723 7797996.2370572 6727675.7578617
1 longitude: -148.003774863594 latitude: 9.20238223616225
    MaxDiff: 14487.6784785809 Distances: 7439006.46128994 7432752.45732905 7447240.13580763
2 longitude: -147.197808459106 latitude: 16.3073233548167
    MaxDiff: 2.32572609744966 Distances: 7441374.94409209 7441377.26981819 7441375.90768183
3 longitude: -147.157734641831 latitude: 16.2233338760411
    MaxDiff: 7.72997736930847E-08 Distances: 7441375.70752842 7441375.70752848 7441375.7075284
3 longitude: -147.157734641831 latitude: 16.2233338760411 Distance 7441375.70752842
Kirk Kuykendall
fuente
¡Esa es una convergencia impresionantemente rápida! (+1)
whuber
Debe utilizar una proyección equidistante azimutal honesta a la bondad centrada en newCenter. En cambio, está utilizando la proyección centrada en el polo N y cambiando el origen a newCenter. Por lo tanto, puede ser accidental que obtenga una solución decente en este caso (¿tal vez porque los puntos están muy juntos?). Sería bueno probarlo con 3 puntos a miles de kilómetros de distancia. Una implementación de la proyección equidistante azimutal se da en mathworks.com/matlabcentral/fileexchange/…
cffk
@cffk La única otra forma que veo para crear una proyección equidistante acimutal centrada en un punto particular es usar el mismo método pero con esriSRProjCS_World_AzimuthalEquidistant en lugar de esriSRProjCS_WGS1984N_PoleAziEqui (o esriSRProjCS_WGS1984E_Pole. Sin embargo, la única diferencia es que se centra en 0,0 en lugar de 0,90 (o 0, -90). ¿Me puede guiar en la realización de una prueba con los ejercicios de matemáticas para ver si esto produce resultados diferentes de una proyección de "honesto a la bondad"?
Kirk Kuykendall
@KirkKuykendall: vea el apéndice de mi primera respuesta.
cffk
1
@KirkKuykendall Entonces, ¿tal vez ESRI implementó una proyección de "honesto a la bondad"? La propiedad clave requerida para que este algoritmo funcione es que las distancias medidas desde el "punto central" son verdaderas. Y esta propiedad es lo suficientemente fácil de verificar. (La propiedad azimutal relativa al punto central es secundaria y solo puede afectar la tasa de convergencia.)
cffk